【问题标题】:How does running several tasks asynchronously on UI thread using async/await work?如何使用 async/await 在 UI 线程上异步运行多个任务?
【发布时间】:2015-03-16 13:04:38
【问题描述】:

我已经阅读(并使用过)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);
}

【问题讨论】:

  • 您是在说将工作编组回 UI 线程吗?
  • 我现在看到这个问题并不像我想象的那么具体,它可以用不同的方式来解释。阅读您的答案@YuvalItzchakov 后,我更好地理解了,在阅读了 Sievajet 的答案后,我发现当没有异步 IO 绑定操作时还有另一个答案。
  • @Andreas 我看到您添加了另一个示例,它仍然有效,因为:没有涉及线程。您是否按照 Yuval 的建议阅读了 blog.stephencleary.com/2013/11/there-is-no-thread.html
  • @Krumelur 是的,我做到了,当您指出这一点时,它现在非常有意义。目前大量阅读。大脑无处不在。谢谢!
  • 要了解更多关于async-await的信息,请阅读my async-await curation上的文章。

标签: c# .net asynchronous async-await threadpool


【解决方案1】:

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

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

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

实际上,Jeffrey 与 MS 合作实现了受他的 AsyncEnumerator 启发的 async/await

【讨论】:

  • 虽然评价不高,但实际上你的回答解释得很好。谢谢。
【解决方案2】:

据我了解,在这种情况下我从不离开 UI 线程,仍然 它异步运行,用户界面仍然响应,我可以开始 同时执行多个任务,因此加快了我的应用程序。 如果我们只使用一个线程,这将如何工作?

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

为了了解如何同时运行多个工作单元,我们需要掌握一个重要事实:异步 IO 绑定操作(几乎)没有用线程做。

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

Stephans 的博文很好地证明了这一点:

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

另外需要注意的是,当使用async-await 模式时,.NET 在幕后为我们做了一些“魔术”。有一个东西叫做“同步上下文”(你可以找到一个相当长的解释here)。此同步上下文负责再次调用 UI 线程上的延续(在第一个 await 之后的代码)(在存在此类上下文的地方)。

编辑:

应该注意的是,同步上下文的魔力也发生在 CPU 绑定操作(实际上对于任何等待对象),所以当您通过 Task.RunTask.Factory.StartNew 使用线程池线程时,这将作为好吧。

【讨论】:

  • 我不认为 OP 是在询问异步方法中的非阻塞 I/O,我认为他是在询问这样一个事实,即不使用 ConfigureAwait(false) 他可以有效地执行多个并发任务访问当前 UI 上下文(即使用当前 UI 线程)。答案可能是并发任务不一定在多个不同的线程上执行。
  • @Andreas 我可能误解了这个问题。也许OP可以澄清一下?
  • 我想我现在明白了。如果我们以同步方式完成 WriteToDisk,我们将开始操作并耐心等待它返回。当异步执行这些操作时,我们开始操作(无论它是什么类型)走开,让操作(通常不需要线程)告诉我们何时完成。它正确(但简化)正确吗?
  • @Andreas 是的,这是异步 IO 工作原理的一个非常简化的版本。尽管如果您在幕后使用线程(例如在使用 Task.Run 调用时),那么您实际上是在使用线程池线程,如果您 @987654330,则与异步 IO 一样,它将将其继续编组到正确的同步上下文@它。
  • 非常好的解释示例和深入文章的链接。谢谢!
【解决方案3】:

TaskParallelLibrary (TPL) 使用 TaskScheduler 可以配置为 TaskScheduler.FromCurrentSynchronizationContext 以返回 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";
}

【讨论】:

  • 嗯。也许我不明白,但在 DoSomeAsyncWork 我仍然可以调用 UI 元素(比如在表单应用程序中)并为其设置值。如果代码在等待时首先编组回上下文,这将如何工作?
  • 你不应该这样做
  • 所以在等待之后,您将返回到它来自的同步上下文
  • 那么如果 await 是非 I/O 或网络操作,我们会使用另一个线程吗?
  • Task.Delay 不使用底层线程来推迟进一步的执行。使用 Task.Run(() => { });确实使用线程。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-01-04
  • 2016-12-16
  • 2016-12-04
  • 2018-06-24
  • 1970-01-01
  • 2020-10-15
  • 2020-03-01
相关资源
最近更新 更多