C#中的非阻塞文件复制


74

如何在不阻塞线程的情况下用C#复制文件?


1
我对关闭感到有些困惑;看来问题很简单。
Casey

请显示您正在使用的代码,并确切说明您遇到的问题。
tgdavies

Answers:


53

异步编程的思想是允许调用线程(假设它是线程池线程)返回到线程池以在异步IO完成时用于其他任务。在幕后,调用上下文填充到数据结构中,并且有1个或多个IO完成线程监视等待完成的调用。当IO完成时,完成线程将调用回到恢复调用上下文的线程池中。这样,只有100个完成线程和几个线程池线程(大部分处于空闲状态),而不是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);
    }
  }
}

不过,我尚未对此进行全面的性能测试。我有点担心,因为如果这么简单,它将已经存在于核心库中。

等待做我在幕后描述的事情。如果您想对它的工作原理有一个大概的了解,可能会有助于理解Jeff Richter的AsyncEnumerator。他们可能并不完全相同,但是想法确实很接近。如果您通过“异步”方法查看调用堆栈,则会在其上看到MoveNext。

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


请您告诉我,这是什么意思(await source.CopyToAsync(destination);)?
Khaleel Hmoz 2013年

2
在内部,标记为aync的方法中的等待操作是等待代码段完成。天真的,我们可以说它阻止了。但是,它并没有真正阻止。诸如Wait()之类的实际阻塞行为会使活动线程停留在执行点。实际上,Await导致线程正在执行的上下文被卡在数据结构中,并允许活动线程返回线程池,该线程池可将其用于其他用途。当await确实返回一个线程池线程(可能不是同一线程)时,它将检索上下文并恢复执行。
csaam 2014年

11
制作这些方法async必须与建立f $&#ing死亡之星一样困难。这个答案已经有2年了……而且什么都没有改变!没有File.CopyAsync,没有File.GetInfoAsync,没有Directory.EnumerateAsync
米格尔·安吉洛

3
如果有人为此担心。微软有一个具有相同代码的示例,因此我认为它必须合法:msdn.microsoft.com/en-us/library/hh159084
Adam Tal

7
请注意,如果您没有明确打开要异步使用的特定提示(并且这样做并没有这样做),那么幕后发生的一切归结为线程上的同步写入池。请参阅DrewNoakes的答案,以确实提供此类提示。
Joe Amenta

31

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

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


在第一行代码之后究竟发生了什么?它是否释放线程,直到从文件中预取数据?
BornToCode

运行时不提供任何保证。这就是我们所有人希望发生的事情:如果可以在不等待外部资源的情况下为请求提供服务,则等待将同步完成。否则,将捕获状态,线程上下文以及所有线程,线程将屈服,并且在请求完成后继续运行。在下面的增强代码中,未捕获线程上下文。这意味着可能会运行与I / O完成池不同的线程。
GregC

13

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

3
这可能会产生误导,如果我们正在使用gui应用,我们想返回捕获的上下文。这应该是用户的决定,更高的级别(await CopyFileAsync().ConfigureAwait(false)
Nekromancer

我同意。基类库团队建议将上下文捕获行为配置推迟到调用方。该代码不捕获上下文。
GregC

2
将缓冲区大小设置为4096会CopyToAsync大大降低写入网络共享时的速度。使用默认值81920是更好的选择,在我的情况下,速度从2 Mbps变为25 Mbps。请参阅此相关问题以获取解释。
user247702

3
@Nekromancer实际上,使用await sourceStream.CopyToAsync().ConfigureAwait(false)此处是正确的,因为其余的方法代码(无关)并不关心它在哪个上下文中运行。您的调用方法使用它自己的await CopyFileAsync()和它自己的ConfigureAwait()true如果未明确设置,则将设置为。
lauxjpn

1
@ user247702根据链接的问题,它应该仅为64K = 65536字节:“将缓冲区大小
lauxjpn

11

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


问题在于,在同一卷上移动文件时,只要更改标头,就会使用不必要的线程。
IllidanS4在2013年

1
@ IllidanS4不幸的是,如果文件足够大,我们可能正在谈论节省几分钟。
凯西

7

您可以使用异步委托

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

链接告诉您异步编码的不同技术


6
我认为问题在于异步操作而不消耗线程。有多种方法可以将工作委派给线程池,大多数方法比此处的机制更容易。
约翰·梅尔维尔

5

你可以做到这一点作为文章建议:

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

2
流现在具有内置的复制操作,使复制变得更加容易。但是,这种技术的问题在于,即使文件位于同一磁盘上,也不需要进行任何操作,但它始终会复制文件。
凯西

2

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


-3

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


4
起作用的原因是,如果要在同一驱动器中移动文件,则除了头文件外,不需要重写任何内容。如果移至其他驱动器,则可以使自己确信这不是异步操作。
Casey

为了扩展Casey的响应,通常通过VPN或WAN复制文件仍然很慢。
克里斯·沃尔什

-5

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

这是您(同步)执行操作的方式:

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

以下是异步执行操作的方法:

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

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

欢呼声

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.