Как nunit успешно ждет завершения асинхронных методов?
При использовании async/await
в С# общее правило состоит в том, чтобы избегать async void
, поскольку это в значительной степени является огнем и забывается, вместо этого следует использовать Task
, если из метода не возвращено возвращаемое значение. Имеет смысл. Странно, однако, что на той неделе, когда я писал несколько модульных тестов для нескольких методов async
, которые я написал, и заметил, что NUnit предложил пометить теги async
как либо void
, либо вернуть Task
. Затем я попробовал, и, конечно же, это сработало. Это выглядело действительно странным, как можно было бы реализовать структуру nunit для запуска метода и дождаться завершения всех асинхронных операций? Если он возвращает "Задача", он может просто ждать задания, а затем делать то, что ему нужно, но как он может его отключить, если он вернет пустоту?
Итак, я взломал исходный код и нашел его. Я могу воспроизвести его в небольшом экземпляре, но я просто не могу понять, что они делают. Наверное, я недостаточно разбираюсь в SynchronizationContext и как это работает. Здесь код:
class Program
{
static void Main(string[] args)
{
RunVoidAsyncAndWait();
Console.WriteLine("Press any key to continue. . .");
Console.ReadKey(true);
}
private static void RunVoidAsyncAndWait()
{
var previousContext = SynchronizationContext.Current;
var currentContext = new AsyncSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(currentContext);
try
{
var myClass = new MyClass();
var method = myClass.GetType().GetMethod("AsyncMethod");
var result = method.Invoke(myClass, null);
currentContext.WaitForPendingOperationsToComplete();
}
finally
{
SynchronizationContext.SetSynchronizationContext(previousContext);
}
}
}
public class MyClass
{
public async void AsyncMethod()
{
var t = Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Done sleeping!");
});
await t;
Console.WriteLine("Done awaiting");
}
}
public class AsyncSynchronizationContext : SynchronizationContext
{
private int _operationCount;
private readonly AsyncOperationQueue _operations = new AsyncOperationQueue();
public override void Post(SendOrPostCallback d, object state)
{
_operations.Enqueue(new AsyncOperation(d, state));
}
public override void OperationStarted()
{
Interlocked.Increment(ref _operationCount);
base.OperationStarted();
}
public override void OperationCompleted()
{
if (Interlocked.Decrement(ref _operationCount) == 0)
_operations.MarkAsComplete();
base.OperationCompleted();
}
public void WaitForPendingOperationsToComplete()
{
_operations.InvokeAll();
}
private class AsyncOperationQueue
{
private bool _run = true;
private readonly Queue _operations = Queue.Synchronized(new Queue());
private readonly AutoResetEvent _operationsAvailable = new AutoResetEvent(false);
public void Enqueue(AsyncOperation asyncOperation)
{
_operations.Enqueue(asyncOperation);
_operationsAvailable.Set();
}
public void MarkAsComplete()
{
_run = false;
_operationsAvailable.Set();
}
public void InvokeAll()
{
while (_run)
{
InvokePendingOperations();
_operationsAvailable.WaitOne();
}
InvokePendingOperations();
}
private void InvokePendingOperations()
{
while (_operations.Count > 0)
{
AsyncOperation operation = (AsyncOperation)_operations.Dequeue();
operation.Invoke();
}
}
}
private class AsyncOperation
{
private readonly SendOrPostCallback _action;
private readonly object _state;
public AsyncOperation(SendOrPostCallback action, object state)
{
_action = action;
_state = state;
}
public void Invoke()
{
_action(_state);
}
}
}
При запуске вышеуказанного кода вы заметите, что сообщения Done Sleeping и Done, ожидающие сообщения, появляются до нажатия любой клавиши для продолжения сообщения, что означает, что метод async каким-то образом ждет.
Мой вопрос: может кто-то может объяснить, что здесь происходит? Что такое SynchronizationContext
(я знаю, что он использовался для публикации работы из одного потока в другой), но я все еще смущен тем, как мы можем ждать выполнения всей работы. Спасибо заранее!
Ответы
Ответ 1
A SynchronizationContext
позволяет переносить работу в очередь, которая обрабатывается другим потоком (или пулом потоков) - обычно для этого используется контур сообщения для интерфейса пользовательского интерфейса.
Функция async
/await
внутренне использует текущий контекст синхронизации для возврата в нужный поток после завершения задачи, которую вы ожидали.
Класс AsyncSynchronizationContext
реализует свой собственный цикл сообщений. Работа, отправленная в этот контекст, добавляется в очередь.
Когда ваша программа вызывает WaitForPendingOperationsToComplete();
, этот метод запускает цикл сообщения, захватывая работу из очереди и выполняя ее.
Если вы установите точку останова на Console.WriteLine("Done awaiting");
, вы увидите, что она работает в основном потоке в методе WaitForPendingOperationsToComplete()
.
Кроме того, функция async
/await
вызывает методы OperationStarted()
/OperationCompleted()
для уведомления SynchronizationContext
, когда метод async void
запускает или завершает выполнение.
AsyncSynchronizationContext
использует эти уведомления, чтобы подсчитать количество запущенных методов async
и еще не завершено. Когда этот счетчик достигнет нуля, метод WaitForPendingOperationsToComplete()
прекратит выполнение цикла сообщения, и поток управления возвращается к вызывающему.
Чтобы просмотреть этот процесс в отладчике, установите точки останова в методах Post
, OperationStarted
и OperationCompleted
контекста синхронизации. Затем выполните вызов AsyncMethod
:
- Когда вызывается
AsyncMethod
,.NET сначала вызывает OperationStarted()
- Это устанавливает
_operationCount
в 1.
- Затем тело
AsyncMethod
запускается (и запускает фоновое задание)
- В операторе
await
AsyncMethod
дает управление, поскольку задача еще не завершена
-
currentContext.WaitForPendingOperationsToComplete();
получает вызов
- В очереди еще нет операций, поэтому основной поток переходит в режим сна
_operationsAvailable.WaitOne();
- В фоновом потоке:
- в какой-то момент задача заканчивается спать
- Выход:
Done sleeping!
- делегат завершает выполнение, и задача становится помеченной как полная
- вызывается метод
Post()
, задерживающий продолжение, представляющее остаток AsyncMethod
- Основной поток просыпается, потому что очередь больше не пуста.
- Цикл сообщения запускает продолжение, тем самым возобновляя выполнение
AsyncMethod
- Выход:
Done awaiting
-
AsyncMethod
завершает выполнение, в результате чего .NET вызывает OperationComplete()
-
_operationCount
уменьшается до 0, что отмечает цикл сообщения как завершенный
- Элемент управления возвращается в цикл сообщения
- Цикл сообщения заканчивается, потому что он был отмечен как полный, а
WaitForPendingOperationsToComplete
возвращает вызывающему абоненту
- Выход:
Press any key to continue. . .