运行 使用 async/await 在 UI 线程上异步执行多个任务如何工作?

How does running several tasks asynchronously on UI thread using async/await work?

一段时间以来,我已经阅读(并使用)了很多async/await,但我仍然有一个问题无法得到答案。假设我有这个代码。

private async void workAsyncBtn_Click(object sender, EventArgs e)
{
    var myTask = _asyncAwaitExcamples.DoHeavyWorkAsync(5);
    await myTask;
    statusTextBox.Text += "\r\n DoHeavyWorkAsync message";
}

它从 UI 线程调用并返回到 UI 线程。因此,我可以在这个方法中和 await myTask 之后做 UI-特定的事情。如果我使用了 .ConfigureAwait(false),我会在执行 statusTextBox.Text += "\r\n DoHeavyWorkAsync message"; 时得到一个线程异常,因为我会告诉 myTask 可以从线程池中获取任何可用线程。

我的问题。据我了解,在这种情况下,我从不离开 UI 线程,它仍然是 运行 异步的, UI 仍然是响应式的,我可以同时启动多个任务,从而加快速度我的应用程序。如果我们只使用一个线程,这怎么能工作呢?

谢谢!

编辑 Sievajet

private async void workAsyncBtn_Click(object sender, EventArgs e)
{
    await DoAsync();
}

private async Task DoAsync()
{
    await Task.Delay(200);
    statusTextBox.Text += "Call to form";
    await Task.Delay(200);
}

As I understand it I never leave the UI thread in this case, still it's run asynchronously, the UI is still responsive and I can start several Tasks at the same time and therefor speed up my application. How can this work if we only use one thread?

首先,我建议阅读 Stephan Clearys 的博客 post - There is no thread

为了理解 运行 多个工作单元如何一起工作,我们需要掌握一个重要事实:async IO 绑定操作有 (almost) 与线程无关。

这怎么可能?好吧,如果我们一直深入到操作系统,我们会看到对 设备驱动程序 的调用 - 那些负责执行诸如网络调用和写入磁盘,都是作为自然异步实现的,它们在工作时不占用线程。这样,当设备驱动程序在做它的事情时,就不需要线程了。只有在设备驱动程序完成执行后,它才会通过 IOCP(I/O 完成端口)通知操作系统它已完成,然后操作系统将执行剩余的方法调用(这是在 .NET 中通过线程池,具有专用的 IOCP 线程)。

Stephans 的博客 post 很好地展示了这一点:

一旦 OS 执行 DPC(延迟过程调用)并将 IRP 排队(I/O 请求数据包),它的工作基本上完成,直到设备驱动程序用 I'm done 消息,这会导致执行整个操作链(在博客 post 中描述),最终将调用您的代码。

另一件需要注意的事情是 .NET 在使用 async-await 模式时在幕后为我们做了一些 "magic"。有一个东西叫"Synchronization Context"(你可以找到一个相当冗长的解释here)。这个同步上下文是 in-charge 在 UI 线程上再次调用延续(第一个 await 之后的代码)的内容(在存在此类上下文的地方)。

编辑:

应该注意的是,同步上下文的魔力也发生在 CPU 绑定操作(实际上是任何可等待的对象),所以当您通过 Task.RunTask.Factory.StartNew,这也行。

TaskParallelLibrary (TPL) 使用 TaskScheduler,它可以配置为 TaskScheduler.FromCurrentSynchronizationContext 到 return 到 SynchronizationContext,如下所示:

textBox1.Text = "Start";
// The SynchronizationContext is captured here
Factory.StartNew( () => DoSomeAsyncWork() )
.ContinueWith( 
    () => 
    {
       // Back on the SynchronizationContext it came from            
        textBox1.Text = "End";
    },TaskScheduler.FromCurrentSynchronizationContext());

async 方法在 await 暂停时,默认情况下它将捕获当前 SynchronizationContext 并在 await 返回它来自的 SynchronizationContext 后编组代码。

        textBox1.Text = "Start";

        // The SynchronizationContext is captured here

       /* The implementation of DoSomeAsyncWork depends how it runs, this could run on the threadpool pool 
          or it could be an 'I/O operation' or an 'Network operation' 
          which doesnt use the threadpool */
        await DoSomeAsyncWork(); 

        // Back on the SynchronizationContext it came from
        textBox1.Text = "End";

异步和等待示例:

async Task MyMethodAsync()
{
  textBox1.Text = "Start";

  // The SynchronizationContext is captured here
  await Task.Run(() => { DoSomeAsyncWork(); }); // run on the threadPool

  // Back on the SynchronizationContext it came from
  textBox1.Text = "End";
}

当 UI 线程调用 await 时,它会立即启动异步操作和 returns。当异步操作完成时,它会通知线程池中的线程,但异步等待的内部实现将执行分派给 UI 线程,该线程将继续执行等待后的代码。

Dispatch 是通过 SynchronizationContext 实现的,后者又调用 System.Windows.Forms.Control.BeginInvoke。

CLR via C#(第 4 版)(开发人员参考)第 4 版,Jeffrey Richter 第 749 页

实际上,Jeffrey 与 MS 合作实现了 async/await 灵感来自于他的 AsyncEnumerator