C# async/await 进度报告未按预期顺序进行
C# async/await progress reporting is not in expected order
我正在试验 async/await 和进度报告,因此编写了一个异步文件复制方法,在每次复制 MB 后报告进度:
public async Task CopyFileAsync(string sourceFile, string destFile, CancellationToken ct, IProgress<int> progress) {
var bufferSize = 1024*1024 ;
byte[] bytes = new byte[bufferSize];
using(var source = new FileStream(sourceFile, FileMode.Open, FileAccess.Read)){
using(var dest = new FileStream(destFile, FileMode.Create, FileAccess.Write)){
var totalBytes = source.Length;
var copiedBytes = 0;
var bytesRead = -1;
while ((bytesRead = await source.ReadAsync(bytes, 0, bufferSize, ct)) > 0)
{
await dest.WriteAsync(bytes, 0, bytesRead, ct);
copiedBytes += bytesRead;
progress?.Report((int)(copiedBytes * 100 / totalBytes));
}
}
}
}
在控制台应用程序中创建随机内容为 10MB 的 I 文件,然后使用上述方法复制它:
private void MainProgram(string[] args)
{
Console.WriteLine("Create File...");
var dir = Path.GetDirectoryName(typeof(MainClass).Assembly.Location);
var file = Path.Combine(dir, "file.txt");
var dest = Path.Combine(dir, "fileCopy.txt");
var rnd = new Random();
const string chars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890");
var str = new string(Enumerable
.Range(0, 1024*1024*10)
.Select(i => letters[rnd.Next(chars.Length -1)])
.ToArray());
File.WriteAllText(file, str);
var source = new CancellationTokenSource();
var token = source.Token;
var progress = new Progress<int>();
progress.ProgressChanged += (sender, percent) => Console.WriteLine($"Progress: {percent}%");
var task = CopyFileAsync(file, dest, token, progress);
Console.WriteLine("Start Copy...");
Console.ReadLine();
}
应用程序执行后,两个文件相同,因此复制过程按正确顺序执行。但是,控制台输出类似于:
Create File...
Start Copy...
Progress: 10%
Progress: 30%
Progress: 20%
Progress: 60%
Progress: 50%
Progress: 70%
Progress: 80%
Progress: 40%
Progress: 90%
Progress: 100%
每次调用应用程序时顺序都不一样。我不明白这种行为。如果我在事件处理程序中放置一个断点并检查每个值,它们的顺序是正确的。谁能给我解释一下?
我想稍后在带有进度条的 GUI 应用程序中使用它,不想让它一直来回跳转。
Progress<T>
在创建时捕获当前 SynchronizationContext
。如果没有 SynchronizationContext
(就像在控制台应用程序中一样)- 进度回调将被安排到线程池线程。这意味着多个回调甚至可以运行并行,当然不能保证顺序。
在 UI 应用程序中,发布到同步上下文大致相当于:
在 WPF 中:Dispatcher.BeginInvoke()
在 WinForms 中:Control.BeginInvoke
我没有使用 WinForms,但在 WPF 中,多个 BeginInvoke
具有相同的优先级(在本例中它们具有相同的优先级)是 guaranteed 执行顺序被调用:
multiple BeginInvoke calls are made at the same DispatcherPriority,
they will be executed in the order the calls were made.
我不明白为什么在 WinForms 中 Control.BeginInvoke
可能会执行我们的订单,但我不知道像我上面为 WPF 提供的证明。所以我认为在 WPF 和 WinForms 中,你可以安全地依赖你的进度回调按顺序执行(前提是你在 UI 线程上创建了 Progress<T>
实例本身,以便可以捕获上下文)。
站点注释:不要忘记将 ConfigureAwait(false)
添加到您的 ReadAsync
和 WriteAsync
调用以防止返回到 UI 应用程序中的 UI 线程每次在 await
s.
之后
我正在试验 async/await 和进度报告,因此编写了一个异步文件复制方法,在每次复制 MB 后报告进度:
public async Task CopyFileAsync(string sourceFile, string destFile, CancellationToken ct, IProgress<int> progress) {
var bufferSize = 1024*1024 ;
byte[] bytes = new byte[bufferSize];
using(var source = new FileStream(sourceFile, FileMode.Open, FileAccess.Read)){
using(var dest = new FileStream(destFile, FileMode.Create, FileAccess.Write)){
var totalBytes = source.Length;
var copiedBytes = 0;
var bytesRead = -1;
while ((bytesRead = await source.ReadAsync(bytes, 0, bufferSize, ct)) > 0)
{
await dest.WriteAsync(bytes, 0, bytesRead, ct);
copiedBytes += bytesRead;
progress?.Report((int)(copiedBytes * 100 / totalBytes));
}
}
}
}
在控制台应用程序中创建随机内容为 10MB 的 I 文件,然后使用上述方法复制它:
private void MainProgram(string[] args)
{
Console.WriteLine("Create File...");
var dir = Path.GetDirectoryName(typeof(MainClass).Assembly.Location);
var file = Path.Combine(dir, "file.txt");
var dest = Path.Combine(dir, "fileCopy.txt");
var rnd = new Random();
const string chars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890");
var str = new string(Enumerable
.Range(0, 1024*1024*10)
.Select(i => letters[rnd.Next(chars.Length -1)])
.ToArray());
File.WriteAllText(file, str);
var source = new CancellationTokenSource();
var token = source.Token;
var progress = new Progress<int>();
progress.ProgressChanged += (sender, percent) => Console.WriteLine($"Progress: {percent}%");
var task = CopyFileAsync(file, dest, token, progress);
Console.WriteLine("Start Copy...");
Console.ReadLine();
}
应用程序执行后,两个文件相同,因此复制过程按正确顺序执行。但是,控制台输出类似于:
Create File...
Start Copy...
Progress: 10%
Progress: 30%
Progress: 20%
Progress: 60%
Progress: 50%
Progress: 70%
Progress: 80%
Progress: 40%
Progress: 90%
Progress: 100%
每次调用应用程序时顺序都不一样。我不明白这种行为。如果我在事件处理程序中放置一个断点并检查每个值,它们的顺序是正确的。谁能给我解释一下?
我想稍后在带有进度条的 GUI 应用程序中使用它,不想让它一直来回跳转。
Progress<T>
在创建时捕获当前 SynchronizationContext
。如果没有 SynchronizationContext
(就像在控制台应用程序中一样)- 进度回调将被安排到线程池线程。这意味着多个回调甚至可以运行并行,当然不能保证顺序。
在 UI 应用程序中,发布到同步上下文大致相当于:
在 WPF 中:
Dispatcher.BeginInvoke()
在 WinForms 中:
Control.BeginInvoke
我没有使用 WinForms,但在 WPF 中,多个 BeginInvoke
具有相同的优先级(在本例中它们具有相同的优先级)是 guaranteed 执行顺序被调用:
multiple BeginInvoke calls are made at the same DispatcherPriority, they will be executed in the order the calls were made.
我不明白为什么在 WinForms 中 Control.BeginInvoke
可能会执行我们的订单,但我不知道像我上面为 WPF 提供的证明。所以我认为在 WPF 和 WinForms 中,你可以安全地依赖你的进度回调按顺序执行(前提是你在 UI 线程上创建了 Progress<T>
实例本身,以便可以捕获上下文)。
站点注释:不要忘记将 ConfigureAwait(false)
添加到您的 ReadAsync
和 WriteAsync
调用以防止返回到 UI 应用程序中的 UI 线程每次在 await
s.