在 `await` 关键字之后,运行代码的是哪个线程?

15

让我举一个简单的例子:

    private void MyMethod()
    {
        Task task = MyAsyncMethod();
        task.Wait();
    }

    private async Task MyAsyncMethod()
    {
        //Code before await
        await MyOtherAsyncMethod();
        //Code after await
    }

假设我在单线程应用程序(如控制台应用程序)中运行上面的代码。我很难理解代码 // await之后的代码 如何能够运行。

我知道当我遇到 await 关键字时,控制权会回到 MyMethod() ,但是我又用 task.Wait() 锁定了线程。如果线程被锁定,那么如果该线程被锁定,如何运行 // await之后的代码

是否会创建一个新线程来运行 // await之后的代码?或者主线程是否会神奇地跳出 task.Wait() 来运行 // await之后的代码

我不确定这个应该怎么工作。

3个回答

17
或许会,或许不会。对于Task的可等待模式实现,它会使用在await表达式开始时的同步上下文来运行继续部分(即await表达式之后的部分)。例如,如果你在UI线程上下文中,那么你将会回到同一个UI线程。如果你在线程池线程中,根据我的经验,你通常会回到某个线程池线程,但不一定是同一个线程。
当然,在你的代码示例中,如果你在UI线程中调用Wait(),它将会阻塞UI线程,导致继续部分无法运行 - 你需要小心处理这个问题。(对于你不知道是否已完成且可能需要在当前线程上执行工作的任务调用Wait()或Result()是一个不好的主意。)
请注意,您可以调用Task.ConfigureAwait,以表达不需要在同一上下文中继续的意图。这通常适用于不关心在哪个线程上运行的库方法。
await task.ConfigureAwait(false);

(它影响的不仅仅是线程 - 它是整个上下文是否被捕获的问题。)
我认为熟悉await的内部工作原理是一个好主意。有很多在线文档可供参考,如果你允许我简短地推荐一下,还有《C#深入解析》的第三版,以及我关于这个主题的Tekpub视频系列。或者你可以从MSDN开始,然后逐步深入。

好的,我理解在 UI 线程上下文中的行为,比如在 WCF 应用程序中或者线程池中的行为,但是在单线程应用程序中,比如控制台应用程序中会发生什么呢? - AxiomaticNexus
1
@Jon:继续操作实际上是排队到当前的“同步上下文”,并回退到当前的“任务调度程序”。这是因为“TaskScheduler.Current”有点古怪,不太直观。 - Stephen Cleary
2
@YasmaniLlanes:实际上,不存在单线程的.NET应用程序;每个.NET应用程序都有线程池。因此,在这种情况下(当没有当前的“同步上下文”或“任务调度程序”时),继续操作将排队到线程池中。 - Stephen Cleary
@Stephen Cleary,您刚刚用那三个句子回答了很多问题。我之前并没有意识到在 .Net 中控制台应用程序后面有一个线程池。现在加入了线程池的概念,一切都变得非常清晰明了。感谢您的回答。 - AxiomaticNexus
@StephenCleary:谢谢 - 我已经编辑,改为指定同步上下文而不是任务调度程序。 - Jon Skeet
@Servy:从公共方面来看,确实只有一个线程池。它仍然会是“来自线程池的线程”,但可能是不同的线程。 - Jon Skeet

16

如果在主线程中调用,发布的代码将在Winform应用程序中造成“死锁”,因为您正在使用Wait()阻塞主线程。

但是,在控制台应用程序中,它可以工作。但是为什么?

答案隐藏在SynchronizationContext.Current中。 await捕获"同步上下文",当任务完成时,它将在同一"同步上下文"中继续进行。

在winform应用程序中,SynchronizationContext.Current将设置为WindowsFormsSynchronizationContext,后者将发布调用到“消息循环”,但是谁将处理它?我们的主线程正在等待Wait()

在控制台应用程序中,默认情况下不会设置SynchronizationContext.Current,因此当没有"SynchronizationContext"可供await捕获时,它将安排继续运行的线程池ThreadPool(ThreadpoolTaskScheduler),因此在await之后的代码可以通过线程池线程运行。

前面提到的捕获行为可以使用Task.ConfigureAwait(false);进行控制,这将防止winform应用程序发生死锁,但是await之后的代码不再在UI线程中运行。


这一切都假设当前的同步上下文从未被用户代码更改;您可以在这些应用程序中的任何一个中设置它,以任何您想要的行为。 - Servy
我之前对于 .Net 控制台应用程序中的线程池一无所知,现在意识到了这一点,所有问题都迎刃而解了。谢谢。 - AxiomaticNexus
1
@YasmaniLlanes 如果你不了解“线程池”,我建议你先去了解一下。应该是这样的,先学习线程池,然后再学习TPL,最后是async/await。 - Sriram Sakthivel
这个怎么会死锁呢?它不是只会冻结直到 task.Wait() 完成吗? - user764754
1
@user764754,我现在有点懒得解释了。我会给你分享一篇博客文章,它解释了为什么会出现死锁。请参考http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html。 - Sriram Sakthivel
显示剩余2条评论

1
假设我在一个单线程的应用程序(比如控制台应用程序)中运行上述代码。
控制台应用程序并不是单线程的。该进程在一个线程上开始运行,然后可能启动无限数量的其他线程,最后在最后一个前台线程退出时终止。对于这种类型的应用程序,"自由线程"可能是一个更准确的分类。
在"await"关键字之后运行代码的是哪个线程?
由于您提到了控制台应用程序,我将完全跳过由同步上下文的存在引起的影响。控制台应用程序没有这样的东西。静态属性SynchronizationContext.Currentnull,这极大地简化了事情,并且使ConfigureAwait配置无关紧要。因为在await点没有同步上下文需要被捕获,所以.ConfigureAwait(false).ConfigureAwait(true)具有完全相同的行为。
await之后运行代码的线程取决于awaiter的IsCompleted属性。
如果TaskAwaiter.IsCompletedtrue,则await之后的线程与await之前的线程相同。这是因为await之后的继续操作没有被调度,而是同步运行。一个典型的已完成的可等待对象的例子是Task.CompletedTaskawait Task.CompletedTask; // await之后,与之前的线程相同 如果TaskAwaiter.IsCompletedfalseawait之后的线程是完成可等待对象的线程。这个线程是由可等待对象的实现选择的,并且不受调用者的控制。几乎所有内置的异步API都在ThreadPool上完成,因此很容易得出(错误的)结论,认为有一些主动机制将继续操作重定向到ThreadPool上。一些在ThreadPool上完成的API的例子包括Task.DelayTask.RunHttpClient.GetStringAsync。一个罕见的反例是使用TaskCreationOptions.LongRunning选项配置的Task.Factory.StartNew。这个API在新创建的后台线程上调用提供的action,而不是在ThreadPool线程上,并在同一线程上完成Task。以下是这种行为的示例: await Task.Factory.StartNew(() => Thread.Sleep(100), CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
Console.WriteLine(Thread.CurrentThread.IsThreadPoolThread); // 输出false
进行这样的实验需要一些意识。确保await将以异步方式完成非常重要,这就是action内部的Thread.Sleep(100)的原因。如果我们在action内部不做任何工作,await可能会继续同步进行,因为TaskAwaiter.IsCompleted将被发现为true。在这种情况下,Thread.CurrentThread.IsThreadPoolThread也将为false,但原因是错误的:它将为false,因为我们仍然在控制台应用程序的主线程上。您可以在这里找到一个更复杂的演示,它明确证明了await的继续运行在等待任务的完成线程上。
如果你想知道如何创建一个不在线程池上完成的异步方法,这里有一个简单的示例:
Task ExecuteAsync()
{
    TaskCompletionSource tcs = new();
    new Thread(() => { Thread.Sleep(100); tcs.SetResult(); }).Start();
    return tcs.Task;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接