簡體   English   中英

C#中的非阻塞文件復制

[英]Non-blocking file copy in C#

如何在不阻塞線程的情況下在 C# 中復制文件?

異步編程的思想是允許調用線程(假設它是線程池線程)在異步 IO 完成時返回線程池以用於其他任務。 在幕后,調用上下文被填充到一個數據結構中,1 個或多個 IO 完成線程監視等待完成的調用。 當 IO 完成時,完成線程調用回恢復調用上下文的線程池。 這樣,而不是 100 個線程阻塞,只有完成線程和一些線程池線程大部分閑置。

我能想到的最好的是:

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

不過,我還沒有對此進行廣泛的性能測試。 我有點擔心,因為如果它這么簡單,它已經在核心庫中了。

await 做我在幕后描述的事情。 如果您想大致了解它是如何工作的,那么理解 Jeff Richter 的 AsyncEnumerator 可能會有所幫助。 它們可能不完全相同,但想法非常接近。 如果您從“異步”方法查看調用堆棧,您會在其上看到 MoveNext。

就移動而言,如果它確實是“移動”而不是復制然后刪除,則它不需要是異步的。 移動是針對文件表的快速原子操作。 如果您不嘗試將文件移動到其他分區,它只會以這種方式工作。

這是一個異步文件復制方法,它向操作系統提示我們正在按順序讀取和寫入,以便它可以在讀取時預取數據並為寫入做好准備:

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);
}

您也可以試驗緩沖區大小。 這是 4096 字節。

我通過@DrewNoakes 稍微增強了代碼(性能和取消):

  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);
  }

雖然在某些情況下您希望避免Task.Run ,但Task.Run(() => File.Move(source, dest)將起作用。值得考慮,因為當文件只是在同一個磁盤中移動時/volume,這是一個幾乎是即時的操作,因為標題被更改但文件內容沒有移動。各種“純”異步方法總是復制流,即使不需要這樣做,結果在實踐中可能會慢一些。

您可以使用異步委托

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
        }
    }

您可以將其稱為:

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

這個鏈接告訴你異步編碼的不同技術

你可以做到這一點作為文章建議:

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);

AFAIK,沒有高級異步 API 來復制文件。 但是,您可以使用Stream.BeginRead/EndReadStream.BeginWrite/EndWrite API 構建自己的 API 來完成該任務。 或者,您可以使用此處的答案中提到的BeginInvoke/EndInvoke方法,但您必須記住,它們不會是非阻塞異步 I/O。 它們只是在單獨的線程上執行任務。

我實現了這個用於復制大文件(備份文件)的解決方案,它非常慢! 對於較小的文件,這不是問題,但對於大文件,只需使用 File.Copy 或帶有參數 /mt(多線程)的 robocopy 的實現。

請注意,復制文件異步仍然是 .net 開發的一個懸而未決的問題: https : //github.com/dotnet/runtime/issues/20695

我建議在 .Net 編程語言中可用的 File Copy IO 函數在任何情況下都是異步的。 在我的程序中使用它來移動小文件后,似乎在實際文件復制完成之前后續指令開始執行。 我猜測可執行文件為 Windows 提供了執行復制的任務,然后立即返回以執行下一條指令——而不是等待 Windows 完成。 這迫使我在調用 copy 之后構建 while 循環,該循環將執行,直到我確認復制完成。

正確的復制方式:使用單獨的線程。

您可能會這樣做(同步):

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

以下是異步執行的方法:

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

這是一種非常幼稚的做事方式。 做得好,解決方案將包括一些事件/委托方法來報告文件副本的狀態,並通知重要事件,如失敗、完成等。

干杯,jrh

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM