C# 中的异步文件复制/移动

问题描述 投票:0回答:10

在 C# 中异步进行文件复制/移动的正确方法是什么?

c# asynchronous
10个回答
58
投票

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

就移动而言,如果它确实是“移动”而不是副本然后删除,则不需要异步。移动是针对文件表的快速原子操作。但只有当您不尝试将文件移动到不同的分区时,它才有效。


43
投票

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

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


16
投票

我通过@DrewNoakes 稍微增强了代码(性能和取消):

  public static Task CopyFileAsync(string sourceFile, string destinationFile, CancellationToken cancellationToken = default)
  {
     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))

        return sourceStream.CopyToAsync(destinationStream, bufferSize, cancellationToken);
  }

.NET6 更新(请参阅 https://devblogs.microsoft.com/dotnet/file-io-improvements-in-dotnet-6/

var openForReading = new FileStreamOptions { Mode = FileMode.Open }; 使用 FileStream source = new FileStream("source.txt", openForReading);

var createForWriting = 新的 FileStreamOptions { 模式 = FileMode.CreateNew, 访问=文件访问.写入, 选项= FileOptions.WriteThrough, BufferSize = 0, // 禁用 FileStream 缓冲 PreallocationSize = source.Length // 预先指定大小 }; 使用 FileStream 目的地 = new FileStream("destination.txt", createForWriting); 源.CopyTo(目标);


13
投票

虽然在某些情况下您希望避免

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


8
投票

您可以使用异步委托

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

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


5
投票

您可以按照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);

2
投票

AFAIK,没有高级异步 API 来复制文件。但是,您可以使用

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


2
投票

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

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


-4
投票

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


-6
投票

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

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

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

以下是异步执行的方法:

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

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

干杯, jrh

© www.soinside.com 2019 - 2024. All rights reserved.