Nicht blockierende Dateikopie in C #

73

Wie kann ich eine Datei in C # kopieren, ohne einen Thread zu blockieren?

user95883
quelle
1
Ich bin ein bisschen verwirrt über die Schließung; Die Frage scheint sehr einfach zu sein.
Casey
Bitte zeigen Sie den von Ihnen verwendeten Code und erklären Sie genau, welches Problem Sie haben.
tgdavies

Antworten:

53

Die Idee der asynchronen Programmierung besteht darin, dem aufrufenden Thread (vorausgesetzt, es handelt sich um einen Thread-Pool-Thread) zu ermöglichen, zur Verwendung für eine andere Aufgabe zum Thread-Pool zurückzukehren, während die asynchrone E / A abgeschlossen ist. Unter der Haube wird der Anrufkontext in eine Datenstruktur gestopft, und ein oder mehrere E / A-Abschluss-Threads überwachen den Anruf, der auf den Abschluss wartet. Wenn IO abgeschlossen ist, ruft der Abschluss-Thread einen Thread-Pool auf, der den Aufrufkontext wiederherstellt. Auf diese Weise werden anstelle von 100 blockierenden Threads nur die Abschluss-Threads und einige Thread-Pool-Threads verwendet, die größtenteils im Leerlauf herumliegen.

Das Beste, was ich mir einfallen lassen kann, ist:

public async Task CopyFileAsync(string sourcePath, string destinationPath)
{
  using (Stream source = File.Open(sourcePath))
  {
    using(Stream destination = File.Create(destinationPath))
    {
      await source.CopyToAsync(destination);
    }
  }
}

Ich habe diesbezüglich jedoch keine umfangreichen Perfektionstests durchgeführt. Ich mache mir ein wenig Sorgen, denn wenn es so einfach wäre, wäre es bereits in den Kernbibliotheken.

Warten Sie, was ich hinter den Kulissen beschreibe. Wenn Sie sich einen Überblick über die Funktionsweise verschaffen möchten, ist es wahrscheinlich hilfreich, Jeff Richters AsyncEnumerator zu verstehen. Sie sind vielleicht nicht ganz die gleiche Linie für Linie, aber die Ideen sind wirklich nah. Wenn Sie sich jemals einen Aufrufstapel mit einer "asynchronen" Methode ansehen, wird MoveNext darauf angezeigt.

Was das Verschieben betrifft, muss es nicht asynchron sein, wenn es wirklich ein "Verschieben" und keine Kopie ist, und dann löschen. Verschieben ist eine schnelle atomare Operation gegen die Dateitabelle. Dies funktioniert jedoch nur, wenn Sie nicht versuchen, die Datei auf eine andere Partition zu verschieben.

csaam
quelle
Können Sie mir bitte sagen, was es bedeutet (warten Sie auf source.CopyToAsync (Ziel);)?
Khaleel Hmoz
2
Intern wartet in einer als aync gekennzeichneten Methode das Warten auf den Abschluss des erwarteten Codeteils. Naiv können wir sagen, es blockiert. Es blockiert jedoch nicht wirklich. Ein echtes Blockierungsverhalten wie Wait () hält den aktiven Thread am Ausführungspunkt fest. Warten wartet tatsächlich darauf, dass der Kontext dessen, was der Thread tut, in einer Datenstruktur steckt und der aktive Thread zum Thread-Pool zurückkehrt, wo er für etwas anderes verwendet werden kann. Wenn "Warten" einen Thread-Pool-Thread zurückgibt (wahrscheinlich nicht derselbe), wird der Kontext abgerufen und die Ausführung fortgesetzt.
Csaam
10
Diese Methoden zu entwickeln asyncmuss so schwierig sein wie das Bauen des Todessterns ... diese Antwort hat jetzt 2 Jahre ... und nichts hat sich geändert! Nein File.CopyAsync, nein File.GetInfoAsync, nein Directory.EnumerateAsync.
Miguel Angelo
3
Wenn sich jemand darüber Sorgen macht. Microsoft hat ein Beispiel mit dem gleichen Code, daher muss es echt sein: msdn.microsoft.com/en-us/library/hh159084(v=vs.110).aspx
Adam Tal
7
Beachten Sie, dass, wenn Sie die Dateien nicht explizit mit einem bestimmten Hinweis öffnen, dass Sie sie asynchron verwenden möchten (und dies nicht), das, was hinter den Kulissen passiert, auf synchrone Schreibvorgänge im Thread hinausläuft Schwimmbad. In der Antwort von DrewNoakes finden Sie einen Hinweis.
Joe Amenta
31

Hier ist eine asynchrone Dateikopiermethode, die dem Betriebssystem Hinweise gibt, dass wir nacheinander lesen und schreiben, damit Daten beim Lesen vorab abgerufen werden können und die Dinge für das Schreiben bereit sind:

public static async Task CopyFileAsync(string sourceFile, string destinationFile)
{
    using (var sourceStream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous | FileOptions.SequentialScan))
    using (var destinationStream = new FileStream(destinationFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous | FileOptions.SequentialScan))
        await sourceStream.CopyToAsync(destinationStream);
}

Sie können auch mit der Puffergröße experimentieren. Hier sind es 4096 Bytes.

Drew Noakes
quelle
Was genau passiert nach der ersten Codezeile? Gibt es den Thread frei, bis das Vorabrufen von Daten aus der Datei abgeschlossen ist?
BornToCode
Die Laufzeit gibt keine Garantie. Wir alle hoffen, dass Folgendes passiert: Wenn die Anforderung bearbeitet werden kann, ohne auf externe Ressourcen zu warten, wird das Warten synchron abgeschlossen. Andernfalls wird der Status erfasst, der Threading-Kontext und alles, der Thread wird nachgeben und die Fortsetzung wird ausgeführt, sobald die Anforderung abgeschlossen ist. In meinem erweiterten Code unten wird der Threading-Kontext nicht erfasst. Dies bedeutet, dass möglicherweise ein anderer Thread als der E / A-Abschlusspool ausgeführt wird.
GregC
13

Ich habe den Code von @DrewNoakes leicht verbessert (Leistung und Stornierung):

  public static async Task CopyFileAsync(string sourceFile, string destinationFile, CancellationToken cancellationToken)
  {
     var fileOptions = FileOptions.Asynchronous | FileOptions.SequentialScan;
     var bufferSize = 4096;

     using (var sourceStream = 
           new FileStream(sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, fileOptions))

     using (var destinationStream = 
           new FileStream(destinationFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize, fileOptions))

        await sourceStream.CopyToAsync(destinationStream, bufferSize, cancellationToken)
                                   .ConfigureAwait(continueOnCapturedContext: false);
  }
GregC
quelle
3
Dies kann irreführend sein, wenn wir an einer GUI-App arbeiten, möchten wir zum erfassten Kontext zurückkehren. Dies sollte Benutzerentscheidung sein, eine Stufe höher (await CopyFileAsync().ConfigureAwait(false)
Nekromancer
Genau. Das Team der Basisklassenbibliothek empfiehlt, die Konfiguration des Kontexterfassungsverhaltens auf den Anrufer zu verschieben. Der Code erfasst keinen Kontext.
GregC
2
Durch Festlegen der Puffergröße auf 4096 in CopyToAsyncwird die Geschwindigkeit beim Schreiben auf eine Netzwerkfreigabe erheblich verringert. Die Verwendung der Standardeinstellung 81920 ist eine bessere Option. In meinem Fall stieg die Geschwindigkeit von 2 Mbit / s auf 25 Mbit / s. Eine Erklärung finden Sie in dieser verwandten Frage .
user247702
3
@Nekromancer Tatsächlich ist die Verwendung await sourceStream.CopyToAsync().ConfigureAwait(false)hier korrekt, da es dem verbleibenden Methodencode (nichts) egal ist, in welchem ​​Kontext er ausgeführt wird. Ihre aufrufende Methode verwendet eine eigene await CopyFileAsync()mit einer eigenen ConfigureAwait(), die festgelegt wird, truewenn sie nicht explizit festgelegt wird.
Lauxjpn
1
@ user247702 Dies sollte laut Ihrer verknüpften Frage nur 64 KB = 65536 Byte sein: "Das Erhöhen der Puffergröße über
Lauxjpn
11

Es gibt zwar einige Umstände, die Sie vermeiden möchten Task.Run, aber es Task.Run(() => File.Move(source, dest)wird funktionieren. Es ist eine Überlegung wert, denn wenn eine Datei einfach auf dieselbe Festplatte / dasselbe Volume verschoben wird, ist dies ein fast sofortiger Vorgang, da die Header geändert werden, der Dateiinhalt jedoch nicht verschoben wird. Die verschiedenen "reinen" asynchronen Methoden kopieren den Stream ausnahmslos, auch wenn dies nicht erforderlich ist, und können daher in der Praxis etwas langsamer sein.

Casey
quelle
Das Problem ist, dass beim Verschieben von Dateien auf demselben Volume und beim einfachen Ändern der Header ein unnötiger Thread verwendet wird.
IllidanS4 unterstützt Monica
1
@ IllidanS4 Das ist bedauerlich, aber wir sprechen möglicherweise davon, einige Minuten zu sparen, wenn Ihre Dateien groß genug sind.
Casey
7

Sie können asynchrone Delegaten verwenden

public class AsyncFileCopier
    {
        public delegate void FileCopyDelegate(string sourceFile, string destFile);

        public static void AsynFileCopy(string sourceFile, string destFile)
        {
            FileCopyDelegate del = new FileCopyDelegate(FileCopy);
            IAsyncResult result = del.BeginInvoke(sourceFile, destFile, CallBackAfterFileCopied, null);
        }

        public static void FileCopy(string sourceFile, string destFile)
        { 
            // Code to copy the file
        }

        public static void CallBackAfterFileCopied(IAsyncResult result)
        {
            // Code to be run after file copy is done
        }
    }

Sie können es nennen als:

AsyncFileCopier.AsynFileCopy("abc.txt", "xyz.txt");

Dieser Link informiert Sie über die verschiedenen Techniken der Asyn-Codierung

SO Benutzer
quelle
6
Ich denke, die Frage war, die Operation asynchron durchzuführen, ohne einen Thread zu verbrauchen. Es gibt mehrere Möglichkeiten, Arbeit an den Threadpool zu delegieren, von denen die meisten einfacher sind als der hier beschriebene Mechanismus.
John Melville
5

Sie können dies tun, wie in diesem Artikel vorgeschlagen:

public static void CopyStreamToStream(
    Stream source, Stream destination,
    Action<Stream, Stream, Exception> completed)
    {
        byte[] buffer = new byte[0x1000];
        AsyncOperation asyncOp = AsyncOperationManager.CreateOperation(null);

        Action<Exception> done = e =>
        {
            if(completed != null) asyncOp.Post(delegate
                {
                    completed(source, destination, e);
                }, null);
        };

        AsyncCallback rc = null;
        rc = readResult =>
        {
            try
            {
                int read = source.EndRead(readResult);
                if(read > 0)
                {
                    destination.BeginWrite(buffer, 0, read, writeResult =>
                    {
                        try
                        {
                            destination.EndWrite(writeResult);
                            source.BeginRead(
                                buffer, 0, buffer.Length, rc, null);
                        }
                        catch(Exception exc) { done(exc); }
                    }, null);
                }
                else done(null);
            }
            catch(Exception exc) { done(exc); }
        };

        source.BeginRead(buffer, 0, buffer.Length, rc, null);
Pablo Retyk
quelle
2
Streams verfügen jetzt über einen integrierten Kopiervorgang, der dies erheblich vereinfacht. Mein Problem bei dieser Technik ist jedoch, dass die Datei immer kopiert wird, auch wenn sie sich auf derselben Festplatte befindet und kein solcher Vorgang erforderlich ist.
Casey
2

AFAIK, es gibt keine asynchrone API auf hoher Ebene zum Kopieren einer Datei. Allerdings können Sie Ihre eigenen API bauen diese Aufgabe mit zu erreichen Stream.BeginRead/EndReadund Stream.BeginWrite/EndWriteAPIs. Alternativ können Sie die BeginInvoke/EndInvokein den Antworten hier erwähnte Methode verwenden. Beachten Sie jedoch, dass die asynchronen E / A-Vorgänge nicht blockiert werden. Sie führen die Aufgabe lediglich in einem separaten Thread aus.

Charles Prakash Dasari
quelle
-3

Ich würde vorschlagen, dass die in den Programmiersprachen .Net verfügbare File Copy IO-Funktion in jedem Fall asynchron ist. Nachdem ich es in meinem Programm zum Verschieben kleiner Dateien verwendet habe, scheinen nachfolgende Anweisungen ausgeführt zu werden, bevor die eigentliche Dateikopie abgeschlossen ist. Ich gehe davon aus, dass die ausführbare Datei Windows die Aufgabe gibt, die Kopie zu erstellen, und dann sofort zurückkehrt, um die nächste Anweisung auszuführen - ohne darauf zu warten, dass Windows fertig ist. Dies zwingt mich, while-Schleifen direkt nach dem Aufruf zum Kopieren zu erstellen, die ausgeführt werden, bis ich bestätigen kann, dass der Kopiervorgang abgeschlossen ist.

Tom
quelle
4
Der Grund dafür ist, dass beim Verschieben einer Datei innerhalb desselben Laufwerks nur die Header neu geschrieben werden müssen. Wenn Sie auf ein anderes Laufwerk wechseln, können Sie sich davon überzeugen, dass es sich nicht um eine asynchrone Operation handelt.
Casey
Und um Caseys Antwort zu erweitern, ist das Kopieren von Dateien über VPN oder WAN im Allgemeinen noch sehr langsam.
Chris Walsh
-4

Der richtige Weg zum Kopieren: Verwenden Sie einen separaten Thread.

So könnten Sie es machen (synchron):

//.. [code]
doFileCopy();
// .. [more code]

So geht's asynchron:

// .. [code]
new System.Threading.Thread(doFileCopy).Start();
// .. [more code]

Dies ist eine sehr naive Art, Dinge zu tun. Gut gemacht, würde die Lösung eine Ereignis- / Delegatenmethode enthalten, um den Status der Dateikopie zu melden und wichtige Ereignisse wie Fehler, Abschluss usw. zu benachrichtigen.

Prost, jrh

jrharshath
quelle