NUnit 3,等待,死锁和UI

NUnit 3, await, dead-lock and UI

问题陈述

我们进行了测试,在某些时候,会导致在当前 nunit 线程上设置 SynchronizationContext。据我所知,将其与 await 混合会导致死锁。 问题是我们到处都将业务逻辑与 UI 关注点混合在一起。不理想,但这不是我现在可以轻易改变的。

代码示例

    [Test]
    public async Task DeadLock()
    {
        // force the creation of a SynchronizationContext
        var form = new Form1();
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
        await Task.Delay(10);
        Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    }

此测试将死锁 (.NET 4.6.1)。我不知道为什么。我的假设是 "becomes" 和 UI 线程中的 nunit 线程在 在安排继续之前必须耗尽的消息队列。 因此,仅出于测试目的,我插入了 call

    System.Windows.Forms.Application.DoEvents();

就在等待之前。奇怪的是:测试将不再 死锁,但延续不是在先前的 SynchronizationContext 上执行,而是在线程池线程上执行(SynchronizationContext.Current == null 和不同的托管线程 ID)!很明显吗? 本质上,添加该调用的行为类似于 'ConfigureAwait(false)'.

有人知道测试死锁的原因吗?

假设它与 nunit 如何等待异步测试完成有关,我想我 运行 整个测试在一个单独的线程中:

    [Test]
    public void DeadLock2()
    {
        Task.Run(
            async () =>
            {
                // force the creation of a SynchronizationContext
                var form = new Form1();
                Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
                //System.Windows.Forms.Application.DoEvents();
                await Task.Delay(10);
                Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
            }).Wait();
    }

但这并不能解决问题。 'await' 永远不会回来。 请注意,我不能使用 ConfigureAwait(false),因为延续中的代码需要在 UI 线程上(尽管它消除了死锁)。

// force the creation of a SynchronizationContext
var form = new Form1();

相信这会在当前版本的 WinForms 中安装 WinFormsSynchronizationContext,但请注意,这在以前的版本中不起作用。您过去必须在安装 SyncCtx 之前创建一个实际的控制句柄。

My assumption was that the nunit thread, which kind of "becomes" the UI thread, has work in the message queue that must be drained before the continuation can be scheduled.

实际上,对于 UI 上下文,延续本身包装在一个委托中,该委托作为一种特殊的消息发布到消息队列。如果UI消息循环不是运行ning,那么根本无法执行

And here is the odd thing: The test will no longer dead-lock, but the continuation is not executed on the previous SynchronizationContext, but instead on a thread-pool thread (SynchronizationContext.Current == null and different managed thread id)! Is that obvious?

这很奇怪。我不确定为什么会这样。

I thought I run the whole test in a separate thread... but that does not solve the problem.

不,因为消息循环也不在该线程上 运行。

ConfigureAwait(false)... removes the dead-lock

是的,因为它将继续安排在线程池线程上,而不是将其排队到 UI 消息循环。

The issue is that we mix business logic with UI concerns all over the place. Not ideal, but this is nothing I can easily change at the moment.

如果您的 "UI concerns" 已被单线程上下文充分解决,那么您可以使用 my AsyncEx library 中的 AsyncContext:

[Test]
public void MyTestMethod()
{
  AsyncContext.Run(async () =>
  {
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    await Task.Delay(10);
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
  });
}

AsyncContext 提供了自己的单线程 SynchronizationContext 和 运行 一种 "main loop" 之类的,但它不是 Win32 消息循环,也不够用于 STA 互操作。

如果您的 "UI concerns" 特别依赖 WinForms 上下文(即,您的代码假定存在 Win32 消息泵、使用 STA 对象或其他),那么您可以使用 WindowsFormsContext (originally distributed as part of the Async CTP),它使用真实的 WinFormsSynchronizationContext 并使用真实的 Win32 消息循环:

[Test]
public async Task MyTestMethod()
{
  await WindowsFormsContext.Run(async () =>
  {
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    await Task.Delay(10);
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
  });
}