Code review: Async void

Zaczynamy od razu od kodu:

    class Foo
    {
        public async void DoAsync()
        {
            await Task.Factory.StartNew(() =>
            {
                Thread.Sleep(2000);
                Console.WriteLine("DoAsync...");
            });
        }
    }

Dlaczego powyższy kod jest bardzo niebezpieczny? Nigdy nie należy używać async w połączeniu z void.
Przede wszystkim, użytkownik takiego kodu nie ma możliwości kontroli nad stworzonym wątkiem. Nie wiadomo, kiedy powyższa metoda zakończy się. Wywołanie będzie zatem wyglądać następująco:

    class Program
    {
        static void Main(string[] args)
        {
            Foo foo = new Foo();
            foo.DoAsync();

            Console.WriteLine("End");
            Console.ReadLine();
        }
    }

DoAsync tworzy nowy wątek i wykonuje jakiś kod w osobnym wątku. Ze względu na to, że metoda nie zwraca wątku, nie można użyć await lub Wait(). Jedynie możemy domyślać się, że po pewnym czasie zadanie zostanie wykonane.

Kolejnym problemem jest brak możliwości przetestowania takiej metody. Skoro niemożliwe jest wyznaczenie momentu zakończenia zadania, napisanie poprawnego i stabilnego testu również nie jest łatwe.

Jeszcze większe problemy będziemy mieli w momencie wyrzucenia wyjątku:

    class Foo
    {
        public async void DoAsync()
        {
            await Task.Factory.StartNew(() =>
            {
                Thread.Sleep(2000);
                throw new ArgumentException();
            });
        }
    }

Załóżmy, że kod klienta wygląda następująco:

        static void Main(string[] args)
        {
            Foo foo = new Foo();
            try
            {
                foo.DoAsync();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }

            Console.WriteLine("End");
            Console.ReadLine();
        }

Powyższy kod nigdy nie złapie wyjątku. Ponieważ DoAsync tworzy nowy wątek i nie czeka na jego zakończenie, wyjątek zostanie wyrzucony bezpośrednio do SynchronizationContext. Wątek jest wyrzucany w losowe miejsce, gdzie nie mamy kontroli. Z tego wynika, że wyjątek spowoduje zakończenie procesu. Metoda async void, zatem może zakończyć działanie procesu (poprzez wyrzucenie wyjątku) bez możliwości jego wyłapania – to bardzo niebezpieczne.

Zdefiniujmy teraz klasę foo w prawidłowy sposób:

    class Foo
    {
        public async Task DoAsync()
        {
            await Task.Factory.StartNew(() =>
            {
                Thread.Sleep(2000);
                throw new ArgumentException();
            });
        }
    }

Dzięki temu, że zwracamy Task, możliwe jest użycie słowa kluczowego await:

       private static async Task TestAsync()
        {
            Foo foo = new Foo();
            try
            {
                await foo.DoAsync();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }

Wyjątek zostanie wyłapany tak jak tego spodziewamy się.
Prawie zawsze zatem należy zwracać Task albo Task<T> – nawet jeśli wersja synchroniczna była typu void. Wyjątek stanowią event handler’y. Wynika to z definicji delegaty EventHandler. Warto jednak zwrócić uwagę, że handler’ów nie testujemy bezpośrednio oraz nie ma sensu owijać ich w try-catch. Jakakolwiek obsługa błędów jest w wewnątrz handler’a. Z tego względu jest to dopuszczalne i bezpieczne.

Wyjątku nie stanowią testy jednostkowe. Poniższy test jest również błędny:

[Test]
public async void Should_...().
{
      await _sut.DoAsync();

      Assert....
}

Prawidłowy test to oczywiście:

[Test]
public async Task Should_...().
{
      await _sut.DoAsync();

      Assert....
}

Framework nUnit w pewnej wersji (nie pamiętaj dokładnie, możliwe, że w 2.6) wspierał async void. Autorzy musieli napisać sporo kodu, aby móc obsługiwać takie testy. Od wersji 3.0 jednak, ta funkcjonalność (która była trochę hack’iem) została usunięta. W wersji 3.0 wyłącznie testy async task są wspierane i mogą być uruchamiane.

6 thoughts on “Code review: Async void”

  1. Do testów używam xUnit, gdzie również jest wsparcie dla “async Task”, problemem jest brak widoczności testów w Test Explorerze w VS,

    Czy ktoś z Was zna jakiś sposób, aby sprawić aby testy z tą sygnaturą były widoczne?

  2. Trzy kwestie:
    1) W jaki sposób teraz wywołać metodę TestAsync? Po prostu
    TestAsync();
    ? Wtedy zaświeci się ostrzeżenie. Czy może:
    TestAsync().Wait();
    ? A może jakoś z pomocą Dispatchera?

    2) Jak poprawnie obsłużyć ‘asynchroniczne’ zdarzenia w aplikacji? Domyślam się, że asynchroniczny kod opakować w metodę typu async, a następnie wywołać podobnie jak wyżej?

    3) Jak poprawnie obsłużyć asynchroniczny kod w komendach? Lepiej napisać asynchroniczną implementację komendy, czy można też próbować coś w stylu:

    var command = new RelayCommand(async () =>
    {
    await MyActionAsync();
    });

    Oczywiście właściwie wszystkie te przypadki można sprowadzić do rozwiązania z posta.

Leave a Reply

Your email address will not be published.