ConfigureAwait(true)始终会返回到原始线程,即使被调用方法在内部使用ConfigureAwait(false)吗?

5

我原本期望在1.2位置回到Thread#1,但实际上没有。在进行异步调用后,是否有方法可以返回UI线程?谢谢。

另外,我无法将顶层方法设置为异步。不确定是否完全采用异步方式能够解决这个问题,但目前我没有这个选择。

class Program
{
    static void Main(string[] args)
    {
        ComputeThenUpdateUI().Wait();
    } 
    static async Task ComputeThenUpdateUI()
    {
        Console.WriteLine($"1.1 {Thread.CurrentThread.ManagedThreadId}");
        await ExpensiveComputingNonUI().ConfigureAwait(true);
        Console.WriteLine($"1.2 {Thread.CurrentThread.ManagedThreadId}");
    } 
    static async Task ExpensiveComputingNonUI()
    {
        Console.WriteLine($"2.1 {Thread.CurrentThread.ManagedThreadId}");
        await Task.Delay(3000).ConfigureAwait(false);
        Console.WriteLine($"2.2 {Thread.CurrentThread.ManagedThreadId}");
        await Task.Delay(3000).ConfigureAwait(false);
        Console.WriteLine($"2.3 {Thread.CurrentThread.ManagedThreadId}");
    }
}
 
 
Output:
1.1 1
2.1 1
2.2 4
2.3 4
1.2 4


这取决于安装的同步上下文,这取决于您使用的“应用程序”类型。默认情况下,控制台应用程序没有同步上下文,因此它们的行为与WPF或ASP.NET等内容不同。 - Bradley Uffner
2个回答

7

回复: .ConfigureAwait(true) 不意味着等待完成后流程将返回到同一线程吗?

不是的,在大多数情况下,没有保证。Stephen Cleary 给出了明确的答复:

"context" 是 SynchronizationContext.Current,除非为 null,在这种情况下,它就是 TaskScheduler.Current。(如果当前没有正在运行的任务,则 TaskScheduler.Current 与线程池任务调度程序TaskScheduler.Default相同)。

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

即仅从例如WPF或Windows Forms的UI线程调用的函数才保证返回到同一线程。如果您确实需要返回到同一线程,则需要进行自定义以确保继续在原始线程上计划

除非这很重要(例如对于UI线程),否则返回到同一线程通常是不可取的,因为这可能会降低性能(如果有争夺线程)并且导致死锁

回复: 无法使顶层 main 方法异步化

这在C#7.1中是可能的:

public static async Task Main(string[] args)
{
    // Can await asynchronous tasks here.
}

感谢您详细的回复。我在示例中简化了真实逻辑,但可能会误导:P 在我正在处理的生产代码中,顶层调用者是一个同步方法,我无法更改其签名,并且需要进行一些非UI相关的计算,然后将结果写入UI元素。所以:1)顶层调用者在UI线程中,2)在ConfigureAwait(true)之后仍然注意到线程不在UI线程中,可能是因为调用方内部有一些ConfigAwait(false)调用... - Carol
你能否更新你的问题,提供一个从你的UI开始的示例,并指定你正在处理的“种类”UI,例如WPF、WinForms、Xamarin等。在较低级别的库代码中使用ConfigureAwait(false)不会影响任何高级别的等待 - 它只是跳过了库调用中任何SynchronizationContext的恢复。如果线程在等待之后不在UI线程中(且没有显式的.ConfigureAwait(false)),这意味着异步调用没有捕获UI Synch上下文。 - StuartLC
问题似乎是你没有从UI线程开始async链,这与你目前的想法不符。 - StuartLC

3
ConfigureAwait是在每个编写的方法中做出的本地决策。 ConfigureAwait(true)表示您想尝试返回到最初输入该方法时运行的同一上下文。
首先,这意味着如果您不知道最初运行的上下文,则尝试返回到该上下文没有太大意义。如果可以在多种上下文中调用,则不应尝试执行任何依赖于上下文的操作。这就是为什么如果您正在编写库,通常建议使用ConfigureAwait(false)。您将不知道调用您的代码的上下文。
其次,这意味着没有“全局”上下文可供返回。如果您的调用者(或链条中更高层次的任何调用者等)已经与他们的原始调用上下文分离,那么(通过此机制)无法返回到该上下文。
如果您想切换到特定上下文(例如UI线程),并且不能保证是您的调用上下文,则需要使用不同的、特定于上下文的机制,例如Dispatcher.Invoke()或适合您环境的其他机制。

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