使用WindowsFormsSynchronizationContext在控制台应用程序中使用async/await可能会发生死锁

6
作为一项学习练习,我正在尝试在控制台应用程序中复现在普通 Windows 表单中发生的 async/await 死锁。我希望以下代码会导致这种情况发生,而事实上它确实会发生。但是即使使用 await 时也会出现不可预期的死锁问题。
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
static class Program
{
    static async Task Main(string[] args)
    {
        // no deadlocks when this line is commented out (as expected)
        SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext()); 
        Console.WriteLine("before");
        //DoAsync().Wait(); // deadlock expected...and occurs
        await DoAsync(); // deadlock not expected...but also occurs???
        Console.WriteLine("after");
    }
    static async Task DoAsync()
    {
        await Task.Delay(100);
    }
}

我主要是想知道为什么会发生这种情况?


我的建议是永远不要尝试在控制台中学习有关多任务处理的任何内容。您必须确保控制台程序保持活动和响应。这是事件循环在表单和WPF上已经为您完成的两件事情。| 我也从不使用如此复杂的同步类。一个简单的锁语句和互斥量就足够了。 - Christopher
3
WindowsFormsSynchronizationContext的工作基础假设是程序使用Application.Run()。这对于“让事情发生”和异步延续都非常重要。特别是延续可以发生在调用Run()的同一线程上,这非常理想,因为UI不是线程安全的。在UWP中更是如此,它使用高度异步的WinRT api,这也是2012年将async/await添加到C#语言中的原因。由于没有调度程序循环,程序注定会卡住。 - Hans Passant
文章位于 https://devblogs.microsoft.com/pfxteam/await-synchronizationcontext-and-console-apps/,提供了适用于控制台应用程序的同步上下文代码。 - NineBerry
你可能会对检查我的WinformsApartment代码感兴趣。 - noseratio - open to work
3个回答

3
WindowsFormsSynchronizationContext 会将其分配的委托发布到一个由 UI 线程服务的 WinForms 消息循环中。但是,您从未设置其中任何一个,并且也没有 UI 线程,因此您发布的任何内容都将被忽略。
因此,您的 await 捕获了一个永远不会运行任何完成的 SynchronizationContext
具体的情况如下:
1.您的 Task 是从 Task.Delay 返回的。 2.主线程开始同步等待该任务完成,使用自旋锁(在 Task.SpinThenBlockingWait 中)。 3.自旋锁超时后,主线程创建一个事件来等待,在 Task 上设置一个继续任务可以触发该事件。 4.Task 完成(您可以看到它已经完成,因为其状态是 RanToCompletion)。 5.Task 尝试完成继续任务,以释放主线程正在等待的事件 (Task.FinishContinuations)。这最终调用 TaskContinuation.RunCallback,它会调用您的 WindowsFormsSynchronizationContext.Post 。 6.但是,Post 不执行任何操作,因此会发生死锁。

3
这是因为 WindowsFormsSynchronizationContext 需要依赖一个标准的 Windows 消息循环。控制台应用程序不会启动这样的循环,因此在WindowsFormsSynchronizationContext中发布的消息将不会被处理,任务连续不会被调用,从而导致程序在第一个await处挂起。你可以通过查询布尔属性Application.MessageLoop来确认消息循环不存在。

获取一个值,该值指示此线程上是否存在消息循环。

要使WindowsFormsSynchronizationContext正常工作,您必须启动消息循环。可以像这样完成:

static void Main(string[] args)
{
    EventHandler idleHandler = null;
    idleHandler = async (sender, e) =>
    {
        Application.Idle -= idleHandler;
        await MyMain(args);
        Application.ExitThread();
    };
    Application.Idle += idleHandler;
    Application.Run();
}

MyMain方法是您当前的Main方法,已经重命名。


更新:事实上,Application.Run方法会自动在当前线程中安装WindowsFormsSynchronizationContext,因此您不需要显式地执行此操作。如果您想要防止这种自动安装,可以在调用Application.Run之前配置属性WindowsFormsSynchronizationContext.AutoInstall

AutoInstall属性确定何时在创建控件或启动消息循环时安装WindowsFormsSynchronizationContext


我不会称之为死锁。如果创建另一个线程来泵送消息循环,应用程序将继续运行。死锁意味着两个线程互相等待对方释放资源。 - Aron
@Aron 我猜你的评论是针对主问题,而不是这个答案。 :-) - Theodor Zoulias

2
我认为这是因为async Task Main只是语法糖。实际上它的样子是这样的:
static void Main(string[] args) => MainAsync(args).GetAwaiter().GetResult();

也就是说,它仍然是阻塞的。由于同步上下文不为空,DoAsync的继续尝试在原始线程上执行。但是线程被卡住了,因为它正在等待任务完成。您可以像这样修复它:

static class Program
{
    static async Task Main(string[] args)
    {
        SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
        Console.WriteLine("before");
        await DoAsync().ConfigureAwait(false); //skip sync.context
        Console.WriteLine("after");
    }
    static async Task DoAsync()
    {
        await Task.Delay(100).ConfigureAwait(false); //skip sync.context
    }
}

1
这需要进一步扩展,以便为那些不了解同步上下文或控制台应用程序没有同步上下文的人提供帮助。如果没有“SynchronizationContext.SetSynchronizationContext”,程序将正常运行。正是由于该调用,在“await”执行后,程序尝试返回到原始同步上下文,即原始线程,而该线程被“GetResult()”阻塞。 - Panagiotis Kanavos
@PanagiotisKanavos 好的,我会扩展一下。我写这个答案是为了回答那位已经知道任务和同步上下文的原始问题的提问者。 - mtkachenko
我理解这一点,但请想象其他人试图理解为什么这个答案有帮助。 - Panagiotis Kanavos
“DoAsync”的继续尝试在原始线程上执行,因为同步上下文不为空。这是正确的吗?当然它会尝试在捕获的“WindowsFormsSynchronizationContext”上继续执行。这也无法解释为什么主线程会停留在“TaskAwaiter.HandleNonSuccessAndDebuggerNotification”中。 - canton7
@NineBerry,你应该写 ConfigureAwait(false)。参见这个问题 https://dev59.com/u7Xna4cB1Zd3GeqPFR7A 。 - mtkachenko

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