Skocz do zawartości

Code review: Async void


DevStart Blogi

Recommended Posts

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 – 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.

Wyświetl pełny artykuł

Link do komentarza
Udostępnij na innych stronach

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Gość
Odpowiedz...

×   Wkleiłeś zawartość bez formatowania.   Usuń formatowanie

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Utwórz nowe...