Reputation: 195
We have tests that, at some point, cause a SynchronizationContext to be set on the current nunit thread. Mixing this with await causes a dead-lock to the best of my knowledge. 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.
[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);
}
This test will dead-lock (.NET 4.6.1). I do not know why. 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. So, for testing purposes only, I inserted call
System.Windows.Forms.Application.DoEvents();
right before the await. 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? Essentially, adding that call appears to behave like 'ConfigureAwait(false)'.
Does anybody know why the test dead-locks?
Assuming that it has to do with how nunit waits for async tests to complete, I thought I run the whole test in a separate thread:
[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();
}
but that does not solve the problem. The 'await' is never coming back. Note that I cannot use ConfigureAwait(false) as there is code in the continuations that needs to be on the UI thread (although it removes the dead-lock).
Upvotes: 0
Views: 1073
Reputation: 457127
// force the creation of a SynchronizationContext
var form = new Form1();
I believe this would install a WinFormsSynchronizationContext
with the current version of WinForms, but be aware that this did not work in previous versions. You used to have to create an actual control handle before the SyncCtx was installed.
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.
Actually, for UI contexts, the continuation itself is wrapped in a delegate that is posted to the message queue as a special kind of message. If the UI message loop is not running, then it cannot be executed at all.
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?
That is odd. I'm not sure why that would happen.
I thought I run the whole test in a separate thread... but that does not solve the problem.
No, because the message loop is not run on that thread either.
ConfigureAwait(false)... removes the dead-lock
Yes, because it schedules the continuation on a thread pool thread rather than queueing it to the UI message loop.
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.
If your "UI concerns" are sufficiently addressed by a single-threaded context, then you can use the AsyncContext
in my AsyncEx library:
[Test]
public void MyTestMethod()
{
AsyncContext.Run(async () =>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
await Task.Delay(10);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
});
}
The AsyncContext
provides its own single-threaded SynchronizationContext
and runs a "main loop" of sorts, but it is not a Win32 message loop, nor is it sufficient for STA interop.
If your "UI concerns" specifically depend a WinForms context (i.e., your code assumes the presence of a Win32 message pump, uses STA objects, or whatever), then you can use WindowsFormsContext
(originally distributed as part of the Async CTP), which uses a real WinFormsSynchronizationContext
and pumps a real Win32 message loop:
[Test]
public async Task MyTestMethod()
{
await WindowsFormsContext.Run(async () =>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
await Task.Delay(10);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
});
}
Upvotes: 3