NUnit 3, ожидание, взаимоблокировка и пользовательский интерфейс
Постановка задачи
У нас есть тесты, которые в какой-то момент заставляют SynchronizationContext быть установленным в текущем потоке nunit. Насколько мне известно, смешивание этого с ожиданием приводит к тупику. Проблема в том, что мы смешиваем бизнес-логику с проблемами пользовательского интерфейса повсюду. Не идеально, но это ничего, что я могу легко изменить в данный момент.
Пример кода
[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). Я не знаю почему. Мое предположение состояло в том, что поток nunit, который "становится" потоком пользовательского интерфейса, имеет работу в очереди сообщений, которая должна быть очищена перед планированием продолжения. Итак, только для целей тестирования, я вставил вызов
System.Windows.Forms.Application.DoEvents();
прямо перед ожиданием И вот странная вещь: тест больше не будет тупиковым, но продолжение не выполняется в предыдущем SynchronizationContext, а вместо этого в потоке пула потоков (SynchronizationContext.Current == null и другой идентификатор управляемого потока)! Это очевидно? По сути, добавление этого вызова ведет себя как "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();
}
но это не решает проблему. "Ожидание" никогда не вернется. Обратите внимание, что я не могу использовать ConfigureAwait(false), поскольку в продолжениях есть код, который должен быть в потоке пользовательского интерфейса (хотя он удаляет мертвую блокировку).
1 ответ
// force the creation of a SynchronizationContext
var form = new Form1();
Я считаю, что это установит WinFormsSynchronizationContext
с текущей версией WinForms, но имейте в виду, что это не работало в предыдущих версиях. Раньше вам приходилось создавать настоящий дескриптор управления до того, как SyncCtx был установлен.
Мое предположение состояло в том, что поток nunit, который "становится" потоком пользовательского интерфейса, имеет работу в очереди сообщений, которая должна быть очищена перед планированием продолжения.
На самом деле, для контекстов пользовательского интерфейса само продолжение заключено в делегат, который публикуется в очереди сообщений как особый вид сообщения. Если цикл сообщений пользовательского интерфейса не запущен, он вообще не может быть выполнен.
И вот странная вещь: тест больше не будет тупиковым, но продолжение не выполняется в предыдущем SynchronizationContext, а вместо этого в потоке пула потоков (SynchronizationContext.Current == null и другой идентификатор управляемого потока)! Это очевидно?
Это странно. Я не уверен, почему это случилось.
Я думал, что я запускаю весь тест в отдельном потоке... но это не решает проблему.
Нет, потому что цикл сообщений также не запускается в этом потоке.
ConfigureAwait (false)... удаляет тупик
Да, потому что он планирует продолжение в потоке пула потоков, а не ставит его в очередь в цикле сообщений пользовательского интерфейса.
Проблема в том, что мы смешиваем бизнес-логику с проблемами пользовательского интерфейса повсюду. Не идеально, но это ничего, что я могу легко изменить в данный момент.
Если ваши "проблемы пользовательского интерфейса" в достаточной степени решены в однопоточном контексте, вы можете использовать AsyncContext
в моей библиотеке AsyncEx:
[Test]
public void MyTestMethod()
{
AsyncContext.Run(async () =>
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
await Task.Delay(10);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
});
}
AsyncContext
обеспечивает собственную однопоточность SynchronizationContext
и выполняет своего рода "основной цикл", но он не является циклом сообщений Win32 и не является достаточным для взаимодействия STA.
Если ваши "проблемы пользовательского интерфейса" конкретно зависят от контекста WinForms (т. Е. Ваш код предполагает наличие обработчика сообщений Win32, использует объекты STA или что-то еще), то вы можете использовать WindowsFormsContext
(изначально распространяется как часть 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);
});
}