Подтвердить что ты не робот

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

4b9b3361

Ответ 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. . .