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), поскольку в продолжениях есть код, который должен быть в потоке пользовательского интерфейса (хотя он удаляет мертвую блокировку).


person Sven    schedule 20.06.2016    source источник


Ответы (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 или что-то еще), тогда вы можете использовать _ 7_ (изначально распространяемый как часть 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);
  });
}
person Stephen Cleary    schedule 20.06.2016
comment
Большое спасибо за подробное объяснение. Ответ принят. - person Sven; 20.06.2016