【发布时间】:2009-05-19 13:14:36
【问题描述】:
如何在 C# 中复制文件而不阻塞线程?
【问题讨论】:
-
我对关闭有点困惑;看起来这个问题很简单。
-
请显示您正在使用的代码并准确说明您遇到的问题。
标签: c# asynchronous
如何在 C# 中复制文件而不阻塞线程?
【问题讨论】:
标签: c# asynchronous
异步编程的想法是允许调用线程(假设它是线程池线程)在异步 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。
就移动而言,如果它真的是“移动”而不是复制然后删除,则不需要异步。移动是针对文件表的快速原子操作。如果您不尝试将文件移动到不同的分区,它只会以这种方式工作。
【讨论】:
async 一定和建造 f$ing 死星一样困难......这个答案已经有 2 年了......而且没有任何改变!没有File.CopyAsync,没有File.GetInfoAsync,没有Directory.EnumerateAsync。
这是一个异步文件复制方法,它向操作系统提示我们正在按顺序读取和写入,以便它可以在读取时预取数据并为写入做好准备:
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);
}
【讨论】:
await CopyFileAsync().ConfigureAwait(false)
CopyToAsync 中将缓冲区大小设置为 4096 会大大降低写入网络共享时的速度。使用默认值 81920 是一个更好的选择,在我的情况下,速度从 2 Mbps 变为 25 Mbps。有关说明,请参阅 this related question。
await sourceStream.CopyToAsync().ConfigureAwait(false) 是正确的,因为剩余的方法代码(无)并不关心它在哪个上下文中运行。你的调用方法使用它自己的await CopyFileAsync() 和它自己的ConfigureAwait(),如果没有明确设置,它将被设置为true。
虽然在某些情况下您希望避免使用Task.Run,但Task.Run(() => File.Move(source, dest) 会起作用。这是值得考虑的,因为当一个文件被简单地移动到同一个磁盘/卷中时,它几乎是一个即时的操作,因为标题改变了,但文件内容没有移动。各种“纯”异步方法总是复制流,即使没有必要这样做,因此在实践中可能会慢很多。
【讨论】:
您可以使用异步委托
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告诉你异步编码的不同技术
【讨论】:
你可以按照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);
【讨论】:
AFAIK,没有用于复制文件的高级异步 API。但是,您可以使用Stream.BeginRead/EndRead 和Stream.BeginWrite/EndWrite API 构建自己的API 来完成该任务。或者,您可以使用此处答案中提到的BeginInvoke/EndInvoke 方法,但您必须记住,它们不会是非阻塞异步 I/O。他们只是在单独的线程上执行任务。
【讨论】:
我实现了这个用于复制大文件(备份文件)的解决方案,它非常慢!对于较小的文件,这不是问题,但对于大文件,只需使用 File.Copy 或带有参数 /mt(多线程)的 robocopy 实现。
请注意,异步复制文件对于 .net 开发仍然是一个未解决的问题: https://github.com/dotnet/runtime/issues/20695
【讨论】:
我建议 .Net 编程语言中可用的文件复制 IO 功能在任何情况下都是异步的。在我的程序中使用它移动小文件后,后续指令似乎在实际文件复制完成之前开始执行。我猜测可执行文件给了 Windows 执行复制的任务,然后立即返回执行下一条指令——而不是等待 Windows 完成。这迫使我在调用 copy 之后构建 while 循环,直到我可以确认复制完成为止。
【讨论】:
正确的复制方式:使用单独的线程。
您可能会这样做(同步):
//.. [code]
doFileCopy();
// .. [more code]
这是异步执行的方法:
// .. [code]
new System.Threading.Thread(doFileCopy).Start();
// .. [more code]
这是一种非常幼稚的做事方式。做得好,解决方案将包括一些事件/委托方法来报告文件副本的状态,并通知失败、完成等重要事件。
干杯, jrh
【讨论】: