在已经在线程池线程上运行的调用上使用ConfigureAwait?

3

我有一个WPF应用程序(因此有一个同步上下文)。在UI线程上我的异步调用中,我使用ConfigureAwait(false)来继续在后台线程上执行。这部分我完全理解。

然而,一旦在后台线程上,是否需要为所有后续调用使用ConfigureAwait(false)

public async void SomeMouseClick(someArgs)
{
    await OnTheBackground().ConfigureAwait(false);
}

public async Task OnTheBackground()
{
    await someClient.Execute();
    // am I on the UI thread now, or on still on a thread pool thread?
}

ConfigureAwait() 只影响继续执行,因此您对 OnTheBackground() 的调用是从调用线程的上下文中调用的 - 此时不会创建新线程。在等待 OnTheBackground() 完成后,继续执行可能在新线程上 - 但如果 OnTheBackground() 中没有阻塞任何内容,则仍可能在调用线程上继续执行。因此,即使在 async 方法中,您也必须小心处理计算密集型任务。 - Matthew Watson
假设 await someClient.Execute(); 使用套接字(因此使用IOCP),并且内部套接字缓冲区中没有任何内容,因此它无法直接完成。如果我理解你的意思正确,那么由于在 SomeMouseClick 中没有继续执行,并且对 someClient.Execute 的调用没有配置继续执行的位置,所以 OnTheBackground 调用将在UI线程上继续执行? - jgauffin
没错。您可以通过在各个位置放置 Debug.WriteLine(Thread.CurrentThread.ManagedThreadId); 来检查这一点,以查看线程运行情况。请注意,在 SomeMouseClick() 中等待 OnTheBackground() 完成后,它将在不同的线程上运行(假设 await someClient.Execute(); 在不同的线程上恢复)。 - Matthew Watson
请将其添加到答案中,以便我可以接受它 :) - jgauffin
1个回答

3
ConfigureAwait() 只影响延续操作,所以你调用 OnTheBackground() 的时候是从调用线程的上下文中调用 - 此时不会创建新线程。
在等待 OnTheBackground() 后完成后,延续操作可能在新线程上运行 - 但如果 OnTheBackground() 中没有任何阻塞操作,则仍可能在调用线程上继续。因此,在异步方法中进行计算密集型操作需要小心!
以下是一个WPF应用程序的示例代码,其中有一个名为 Button 的单个按钮,其单击事件通过 Button_Click() 方法处理:
bool firstTime = true;

private async void Button_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("Button_Click() called on thread " + Thread.CurrentThread.ManagedThreadId);

    await OnTheBackground().ConfigureAwait(false);

    Debug.WriteLine("Button_Click() continued on thread " + Thread.CurrentThread.ManagedThreadId);
}

public async Task OnTheBackground()
{
    Debug.WriteLine("OnTheBackground() called on thread " + Thread.CurrentThread.ManagedThreadId);

    if (firstTime)
        await Task.Delay(0);
    else
        await Task.Delay(10);

    firstTime = false;

    Debug.WriteLine("OnTheBackground() continued on thread " + Thread.CurrentThread.ManagedThreadId);
}

当您首次单击该按钮时,它将在OnTheBackground()内调用await Task.Delay(0)。因为这不需要阻塞,所以当await Task.Delay(0)返回时,它会继续在调用它的同一个线程上运行。
然后,当它返回到Button_Click()时,即使其中的await带有.ConfigureAwait(false),它也将继续在原始UI线程上运行,因为它调用的任何内容都没有转换到不同的线程。
因此,在第一次按下按钮时,调试窗口中的输出大致如下:
Button_Click() called on thread 1
OnTheBackground() called on thread 1
OnTheBackground() continued on thread 1
Button_Click() continued on thread 1

一切都在同一个线程上,这是预期的。

然而,在第二次按下按钮时,它最终会调用await Task.Delay(10);,这将导致等待在一个新线程上返回,因为Task.Delay(10)将被阻塞。

因此,当它回到await OnTheBackground().ConfigureAwait(false);时,它将不会继续在UI线程上进行,而是继续在另一个线程上进行。

因此,在第二次按下按钮时,调试输出类似于:

Button_Click() called on thread 1
OnTheBackground() called on thread 1
OnTheBackground() continued on thread 1
Button_Click() continued on thread 4

在这种情况下,您可以看到在Button_Click()await之后的代码现在正在非UI线程上运行。

第二次点击的输出不应该是:
Button_Click() 在线程1上被调用
OnTheBackground() 在线程1上被调用
OnTheBackground() 在线程4上继续执行
Button_Click() 在线程4上继续执行 吗?
- Michael Gehling
@MichaelGehling 不是的 - 调用 await Task.Delay(10); 缺少 .ConfigureAwait(false),因此它将继续在调用它的上下文中执行。(您可以通过运行示例代码并检查调试器中的输出来验证这一点。) 我实际上是从调试窗口复制粘贴这些结果,以避免错误!;) - Matthew Watson
@MichaelGehling 尽管您可能会想知道为什么,即使 OnTheBackground() 在 UI 线程上继续执行,调用 await OnTheBackground().ConfigureAwait(false); 也会在不同的线程上继续执行。这是由 async/await 生成的状态机来管理事物的方式。关于此问题,Jon Skeet 的 "eduasync" 博客中有很多信息(向下滚动到文章开头)。 - Matthew Watson
好的,我尝试了一下,你是对的...我期望configureAwait(false)会在调用树下生效,这样Task.Delay(10)就可以在另一个线程上继续执行。所以如果我想让所有后续的awaits都能够在另一个线程上继续执行,我要么必须在每个await调用上放置configureAwait(false),要么在开头删除SynchronizationContext并在之后重置它,对吗? - Michael Gehling
@MichaelGehling 在Microsoft异步/等待最佳实践指南中有一些关于这个问题的讨论。总的来说,“在可能的情况下使用.ConfigureAwait(false),特别是在库代码中”。通常避免使用.ConfigureAwait(false)的地方是在顶级代码或UI代码中,在这些地方您希望在“等待”之后访问UI组件。 - Matthew Watson

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