C#async/await进度报告不是预期的顺序

Buc*_*ter 6 c# asynchronous progress async-await

我正在尝试使用async/await和progress报告,因此编写了一个异步文件复制方法,在每次复制的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));
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

在控制台应用程序中创建一个随机内容为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();
}
Run Code Online (Sandbox Code Playgroud)

应用程序执行后,两个文件都是相同的,因此复制过程按正确的顺序执行.但是,控制台输出类似于:

Create File...
Start Copy...
Progress: 10%
Progress: 30%
Progress: 20%
Progress: 60%
Progress: 50%
Progress: 70%
Progress: 80%
Progress: 40%
Progress: 90%
Progress: 100%
Run Code Online (Sandbox Code Playgroud)

每次调用应用程序时,订单都不同.我不明白这种行为.如果我将一个断点放到事件处理程序并检查每个值,它们的顺序是正确的.任何人都可以向我解释这个吗?

我想稍后在带有进度条的GUI应用程序中使用它,并且不希望它一直在前后跳转.

Evk*_*Evk 3

Progress<T>SynchronizationContext创建时捕获当前。如果没有SynchronizationContext(如在控制台应用程序中) - 进度回调将被安排到线程池线程。这意味着多个回调甚至可以并行运行,当然顺序不能保证。

在 UI 应用程序中,发布到同步上下文大致相当于:

  1. 在 WPF 中:Dispatcher.BeginInvoke()

  2. 在 WinForm 中:Control.BeginInvoke

我没有使用 WinForms,但在 WPF 中,多个BeginInvoke具有相同优先级的(在本例中它们具有相同的优先级)保证按照调用顺序执行:

以相同的 DispatcherPriority 进行多个 BeginInvoke 调用,它们将按照调用的顺序执行。

我不明白为什么在 WinForms 中Control.BeginInvoke可能会执行我们的顺序,但我不知道像我上面为 WPF 提供的证明。因此,我认为在 WPF 和 WinForms 中,您可以安全地依赖按顺序执行的进度回调(前提是您Progress<T>在 UI 线程上创建了实例本身,以便可以捕获上下文)。

站点注意:不要忘记添加ConfigureAwait(false)到您的ReadAsyncWriteAsync调用中,以防止每次在这些awaits 之后返回到 UI 应用程序中的 UI 线程。