我以为"await"会在同一个线程上继续执行,但似乎并不是这样。

30
我认为async/await的一个要点是,当任务完成时,继续运行同一上下文中await被调用的地方,这在我的情况下可能是UI线程。
例如:
Debug.WriteLine("2: Thread ID: " + Thread.CurrentThread.ManagedThreadId);
await fs.ReadAsync(data, 0, (int)fs.Length);
Debug.WriteLine("3: Thread ID: " + Thread.CurrentThread.ManagedThreadId);

我不会期望这个:

2: Thread ID: 10
3: Thread ID: 11

怎么回事?为什么延续的线程ID与UI线程不同?

根据这篇文章[^],我需要显式调用ConfigureAwait来改变延续上下文的行为!


1
你能否编写一个最小化的复现代码并在此处或Gist中发布? - Stephen Cleary
你在这段代码片段之前使用了 ConfigureAwait(false) 吗?或者这段代码片段是在异步 Lambda 中使用的,像这样:await Task.Run(async () => { /* no synchronization context here! */ }) - noseratio - open to work
1
不,直到我开始谷歌这个问题,我甚至都不知道ConfigureAwait。 - Marc Clifton
3个回答

42

当你使用await时,默认情况下,await运算符将捕获当前的"上下文(context)"并用它来恢复async方法。

这个"上下文"是SynchronizationContext.Current,除非它是null,否则它就是TaskScheduler.Current。(如果没有正在运行的任务,则TaskScheduler.Current与线程池任务调度程序TaskScheduler.Default相同)。

需要注意的是,SynchronizationContextTaskScheduler不一定意味着特定的线程。UI的SynchronizationContext会将工作安排到UI线程;但ASP.NET的SynchronizationContext不会将工作安排到特定的线程。

我怀疑您的问题的原因是您太早地调用了async代码。当应用程序启动时,它只有一个普通的线程。只有当它执行类似于Application.Run的操作时,该线程才成为UI线程。


哇 - 这正是它 -- 通常情况下,当用户单击文件夹并且 UI 正在运行时,该函数会被调用,但我也会在 Application.Run 之前调用此函数以使用根目录进行初始化!我没有想到这一点 - 非常感谢你! - Marc Clifton
你好。https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html 上说,ASP.NET默认情况下也会在调用线程上返回。这是一个特定的线程,只是请求与请求之间有所不同。为什么ASP.NET默认情况下会这样做?这是否与RequestContext的关系有关? - eran otzap
@eranotzap:该博客文章指出ASP.NET(经典版)在请求上下文(而不是特定线程)上恢复。请求上下文管理诸如HttpContext.Current和当前区域设置之类的内容。ASP.NET Core具有更好的设计,不需要SynchronizationContext:https://blog.stephencleary.com/2017/03/aspnetcore-synchronization-context.html - Stephen Cleary
@StephenCleary 我的问题与ASP.net mvc有关。如果没有使用ConfigureAwait(false),那么await将在调用线程上返回,对吗? - eran otzap
1
@eranotzap:可能会,也可能不会。它将在具有相同HttpContext.Current、文化等的线程上恢复。这个线程可能是之前运行代码的同一个线程,也可能不是。 - Stephen Cleary
@StephenCleary 非常好的回答,谢谢。我从博客文章中没有理解到这一点。这是一篇我发送给同事们提醒他们使用ConfigureAwait(false)的博客文章。 - eran otzap

6
await表达式将使用SynchronizationContext.Current的值将控制流返回到发生在其上的线程。 在这种情况下,如果为null,它将默认为TaskScheduler.Current。 该实现仅依靠此值在Task值完成时更改线程上下文。 看起来,在这种情况下,await捕获了与UI线程没有绑定的上下文。

3
实际上,如果 SynchronizationContext.Currentnull,那么 await 将使用 TaskScheduler.Current。而这两个调度程序都不一定会返回到相同的线程。但我同意OP可能没有从UI线程中调用它。 - Stephen Cleary
1
我有点晚参加这个聚会,但是我该如何强制在同一线程上恢复执行?在调用Task.Delay后,我的函数总是在不同的线程上恢复执行,我必须防止这种情况发生。 - Benni

-1

默认情况下,当第一个控件被创建时,Windows Forms会设置同步上下文。

因此,您可以这样做:

[STAThread]
static void Main()
{
    Application.SetHighDpiMode(HighDpiMode.SystemAware);
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    var f = new Form1();
    //var s=SynchronizationContext.Current;
    //var s2=TaskScheduler.Current;
    GetAsync();
    Application.Run(f);
}

static async Task<bool> GetAsync()
{
    MessageBox.Show($"thread id {Thread.CurrentThread.ManagedThreadId}");
    bool flag = await Task.Factory.StartNew<bool>(() =>
    {
       Thread.Sleep(1000);
       return true;
    });
    MessageBox.Show($"thread id {Thread.CurrentThread.ManagedThreadId}");
    return flag;
}

在我看来,Task.Factory.StartNew+Thread.Sleep是一个不好的例子。最好只使用await Task.Delay(1000); - Theodor Zoulias

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