Jak nunit pomyślnie czeka na zakończenie metody async void?

Podczas używania async/await W C#, ogólną zasadą jest unikanie async void, ponieważ jest to prawie ogień i zapominanie, a raczej Task powinno być używane, jeśli nie jest wysyłana żadna wartość zwracana z metody. To ma sens. Dziwne jest jednak to, że na początku tygodnia pisałem testy jednostkowe dla kilku metod async, które napisałem, i zauważyłem, że NUnit zasugerował, aby oznaczyć async testy jako void lub zwracając Task. Potem spróbowałem i na pewno zadziałało. To wydawało się naprawdę dziwne, jak nunit framework czy można uruchomić metodę i czekać na zakończenie wszystkich operacji asynchronicznych? Jeśli zwróci zadanie, może po prostu poczekać na zadanie, a następnie zrobić to, co musi zrobić, ale jak może to zrobić, jeśli zwróci void?

Więc złamałem kod źródłowy i znalazłem go. Mogę to odtworzyć w małej próbce, ale po prostu nie mogę zrozumieć, co robią. Chyba za mało wiem o SynchronizationContext i jak to działa. Oto kod:

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);
        }
    }
}

Kiedy uruchamiając powyższy kod, zauważysz, że wiadomości Done Sleeping i Done waiting pojawiają się przed naciśnij dowolny klawisz, aby kontynuować wiadomość, co oznacza, że metoda asynchroniczna jest w jakiś sposób czekana.

Moje pytanie brzmi, czy ktoś może wyjaśnić, co się tu dzieje? Czym dokładnie jest SynchronizationContext (wiem, że służy do publikowania prac z jednego wątku do drugiego), ale nadal jestem zdezorientowany, jak możemy czekać na całą pracę do wykonania. Z góry dzięki!!
Author: BFree, 2013-02-22

1 answers

A SynchronizationContext pozwala na wysyłanie prac do kolejki, która jest przetwarzana przez inny wątek (lub przez pulę wątków) - zwykle używana jest do tego pętla komunikatów frameworka interfejsu użytkownika. Na async/await funkcja wewnętrznie używa bieżącego kontekstu synchronizacji, aby powrócić do właściwego wątku po zakończeniu zadania, na które czekałeś.

Klasa AsyncSynchronizationContext implementuje własną pętlę komunikatów. Praca, która zostanie opublikowana w tym kontekście, zostanie dodana do kolejki. Gdy twój program wywoła WaitForPendingOperationsToComplete();, ta metoda uruchamia się pętli komunikatów, pobierając pracę z kolejki i wykonując ją. Jeśli ustawisz punkt przerwania na Console.WriteLine("Done awaiting");, zobaczysz, że działa on w głównym wątku w metodzie WaitForPendingOperationsToComplete().

Dodatkowo async/await funkcja wywołuje OperationStarted() / OperationCompleted() metody powiadamiające SynchronizationContext o każdym uruchomieniu lub zakończeniu wykonywania metody async void.

The AsyncSynchronizationContext używa tych powiadomień, aby zapisać liczbę metod async, które są uruchomione i nie zostały jeszcze zakończone. Gdy liczba ta osiągnie zero, metoda WaitForPendingOperationsToComplete() przestaje uruchamiać pętlę wiadomości, a przepływ sterowania powraca do wywołującego.

Aby wyświetlić ten proces w debuggerze, Ustaw punkty przerwania w Post, OperationStarted i OperationCompleted metody kontekstu synchronizacji. Następnie przejdź przez wywołanie AsyncMethod:

    Po wywołaniu. NET pierwsze wywołanie OperationStarted()
    • to ustawia {[22] } na 1.
  • wtedy ciało AsyncMethod zaczyna działać (i uruchamia zadanie w tle)
  • W await w przypadku, gdy zadanie nie jest jeszcze ukończone]}
  • currentContext.WaitForPendingOperationsToComplete(); gets called
  • żadne operacje nie są jeszcze dostępne w kolejce, więc główny wątek przechodzi w tryb uśpienia w _operationsAvailable.WaitOne();
  • W Tle wątku:
    • w pewnym momencie zadanie kończy się spaniem
    • wyjście: Done sleeping!
    • delegat kończy wykonanie, a zadanie zostaje oznaczone jako ukończone
    • metoda Post() zostaje wywołana, pytając o kontynuację, która reprezentuje resztę z AsyncMethod
  • główny wątek budzi się, ponieważ kolejka nie jest już pusta
  • pętla komunikatów uruchamia kontynuację, tym samym wznawia wykonywanie AsyncMethod
  • wyjście: Done awaiting
  • [19]} kończy wykonanie, powodując wywołanie. NET]}
    • _operationCount jest zmniejszona do 0, co oznacza pętlę wiadomości jako kompletną
  • Kontrola powraca do pętli komunikatów
  • pętla wiadomości kończy się, ponieważ została oznaczona jako w tym celu należy skontaktować się z Działem obsługi klienta.]}
  • wyjście: Press any key to continue. . .
 26
Author: Daniel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-02-22 20:19:26