【问题标题】:Non-blocking file copy in C#C#中的非阻塞文件复制
【发布时间】:2009-05-19 13:14:36
【问题描述】:

如何在 C# 中复制文件而不阻塞线程?

【问题讨论】:

  • 我对关闭有点困惑;看起来这个问题很简单。
  • 请显示您正在使用的代码并准确说明您遇到的问题。

标签: c# asynchronous


【解决方案1】:

异步编程的想法是允许调用线程(假设它是线程池线程)在异步 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。

就移动而言,如果它真的是“移动”而不是复制然后删除,则不需要异步。移动是针对文件表的快速原子操作。如果您不尝试将文件移动到不同的分区,它只会以这种方式工作。

【讨论】:

  • 请告诉我,这是什么意思(await source.CopyToAsync(destination);)?
  • 在内部,await 在标记为 aync 的方法中所做的是等待等待的代码片段完成。我们可以天真地说它阻塞。但是它并没有真正阻止。真正的阻塞行为,如 Wait(),使活动线程停留在执行点。 Await 实际上会导致线程正在执行的任何操作的上下文被卡在数据结构中,并允许活动线程返回线程池,在那里它可以用于其他事情。当 await 确实返回一个线程池线程(可能不是同一个)时,将检索上下文并恢复执行。
  • 制作这些方法async 一定和建造 f$ing 死星一样困难......这个答案已经有 2 年了......而且没有任何改变!没有File.CopyAsync,没有File.GetInfoAsync,没有Directory.EnumerateAsync
  • 如果有人担心这个。微软有一个代码相同的例子,所以我猜它一定是合法的:msdn.microsoft.com/en-us/library/hh159084(v=vs.110).aspx
  • 请注意,如果您没有明确打开文件并带有特定提示您将异步使用它(这不会这样做),那么幕后会发生什么归结为线程池上的同步写入。有关确实提供此类提示的内容,请参阅 DrewNoakes 的答案。
【解决方案2】:

这是一个异步文件复制方法,它向操作系统提示我们正在按顺序读取和写入,以便它可以在读取时预取数据并为写入做好准备:

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 字节。

【讨论】:

  • 第一行代码之后到底发生了什么?在从文件中预取数据之前,它会释放线程吗?
  • 运行时不做任何保证。这是我们都希望发生的事情:如果可以在不等待外部资源的情况下处理请求,那么 await 将同步完成。否则状态将被捕获,线程上下文和所有线程将产生,一旦请求完成,将继续运行。在我下面的增强代码中,未捕获线程上下文。这意味着可能会运行与 I/O 完成池不同的线程。
【解决方案3】:

我已经通过@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);
  }

【讨论】:

  • 这可能会产生误导,如果我们正在开发 gui 应用程序,我们希望返回捕获的上下文。这应该是用户决定,更高级别(await CopyFileAsync().ConfigureAwait(false)
  • 我同意。基类库团队建议将上下文捕获行为配置推迟到调用者。代码不捕获上下文。
  • CopyToAsync 中将缓冲区大小设置为 4096 会大大降低写入网络共享时的速度。使用默认值 81920 是一个更好的选择,在我的情况下,速度从 2 Mbps 变为 25 Mbps。有关说明,请参阅 this related question
  • @Nekromancer 实际上,在这里使用await sourceStream.CopyToAsync().ConfigureAwait(false) 是正确的,因为剩余的方法代码(无)并不关心它在哪个上下文中运行。你的调用方法使用它自己的await CopyFileAsync() 和它自己的ConfigureAwait(),如果没有明确设置,它将被设置为true
  • @user247702 根据您的链接问题,这应该只有 64K = 65536 字节:“在任何情况下,将缓冲区大小增加到 64k 以上都无济于事,因为底层 SMB 协议不支持缓冲区长度超过 64k。"
【解决方案4】:

虽然在某些情况下您希望避免使用Task.Run,但Task.Run(() => File.Move(source, dest) 会起作用。这是值得考虑的,因为当一个文件被简单地移动到同一个磁盘/卷中时,它几乎是一个即时的操作,因为标题改变了,但文件内容没有移动。各种“纯”异步方法总是复制流,即使没有必要这样做,因此在实践中可能会慢很多。

【讨论】:

  • 问题是当移动同一个卷上的文件并且它只是改变标题时,这使用了一个不必要的线程。
  • @IllidanS4 很遗憾,但如果您的文件足够大,我们可能会讨论节省几分钟。
【解决方案5】:

您可以使用异步委托

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

这个link告诉你异步编码的不同技术

【讨论】:

  • 我认为问题是异步执行操作,而不消耗线程。有多种方法可以将工作委托给线程池,其中大部分都比这里的机制更容易。
【解决方案6】:

你可以按照this文章的建议来做:

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

【讨论】:

  • 流现在有一个内置的复制操作,这使得这更容易。但我对这种技术的问题是它总是复制文件,即使它在同一个磁盘上并且不需要这样的操作。
【解决方案7】:

AFAIK,没有用于复制文件的高级异步 API。但是,您可以使用Stream.BeginRead/EndReadStream.BeginWrite/EndWrite API 构建自己的API 来完成该任务。或者,您可以使用此处答案中提到的BeginInvoke/EndInvoke 方法,但您必须记住,它们不会是非阻塞异步 I/O。他们只是在单独的线程上执行任务。

【讨论】:

    【解决方案8】:

    我实现了这个用于复制大文件(备份文件)的解决方案,它非常慢!对于较小的文件,这不是问题,但对于大文件,只需使用 File.Copy 或带有参数 /mt(多线程)的 robocopy 实现。

    请注意,异步复制文件对于 .net 开发仍然是一个未解决的问题: https://github.com/dotnet/runtime/issues/20695

    【讨论】:

      【解决方案9】:

      我建议 .Net 编程语言中可用的文件复制 IO 功能在任何情况下都是异步的。在我的程序中使用它移动小文件后,后续指令似乎在实际文件复制完成之前开始执行。我猜测可执行文件给了 Windows 执行复制的任务,然后立即返回执行下一条指令——而不是等待 Windows 完成。这迫使我在调用 copy 之后构建 while 循环,直到我可以确认复制完成为止。

      【讨论】:

      • 有效的原因是,如果您在同一个驱动器中移动文件,除了标题之外,不需要重写任何内容。如果您移动到不同的驱动器,您可以说服自己这不是异步操作。
      • 为了扩展 Casey 的响应,通常通过 VPN 或 WAN 复制文件仍然很慢。
      【解决方案10】:

      正确的复制方式:使用单独的线程。

      您可能会这样做(同步):

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

      这是异步执行的方法:

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

      这是一种非常幼稚的做事方式。做得好,解决方案将包括一些事件/委托方法来报告文件副本的状态,并通知失败、完成等重要事件。

      干杯, jrh

      【讨论】:

        猜你喜欢
        • 2021-12-26
        • 2016-01-02
        • 1970-01-01
        • 1970-01-01
        • 2013-05-24
        • 1970-01-01
        • 2012-12-24
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多