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);
  });
}
Другие вопросы по тегам