Skocz do zawartości

Code Review: Metody asynchroniczne z async oraz oczekiwanie na rezultat


DevStart Blogi

Recommended Posts

Coraz więcej API dostarcza asynchroniczne wersje metod. Niektóre z nich, idą o krok dalej i w ogóle nie posiadają synchronicznej wersji. Załóżmy, że zewnętrzna biblioteka ma następującą metodę:

 async Task<string> FetchDataAsync() {...}

Często jednak nie potrzebujemy korzystać z wersji async i tylko ona komplikuje sprawę. W powyższym przypadku moglibyśmy pokusić się o:

  string data=dataProvider.FetchDataAsync().Result;

  Console.WriteLine(data);

Niestety powyższy kod może być niebezpieczny i wywołać w niektórych sytuacjach deadlock. Jeśli nie wiemy, jak została zaimplementowana metoda FetchDataAsync bardzo łatwo popełnić błąd. Załóżmy, że ciało metody wygląda następująco:

    public async Task<string> FetchDataAsync()
    {
         await DoSomethingAsync();

         return "Hello World";
    }
    private Task DoSomethingAsync()
    {
         return Task.Run(() => Thread.Sleep(2000));
    } 

Kiedyś na blogu pokazywałem już podobny przykład odnoście wydajności async\await. Jeśli wywołujemy await, pobierany jest kontekst przed wejściem w nowy wątek. Dzięki temu, po zakończeniu wątku, czyli w momencie wyjścia z await, wiadomo jaki wątek powinien kontynuować operację. Wynika to z faktu, że w większości przypadków, użytkownik spodziewa się, że wątek przed await i po jest taki sam. Rozważmy kod:

string data = await GetInfo();
_textBox.Text = data;

Gdyby po wyjściu await, kontekst wykonywania nie przechodził w ten przed wywołaniem wątku, wtedy aktualizacja interfejsu nie powiodłaby się – zawsze należy go aktualizować z wątku głównego.

Wracając do przykładu z deadlock. W celu powrócenia do wątku głównego, po wywołaniu “await DoSomethingAsync();”, należy oczywiście uzyskać do niego dostęp. Problem w tym, że będzie on ciągle zajęty. Wywołanie “string data=dataProvider.FetchDataAsync().Result;” blokuje wątek główny do momentu zakończenia operacji. Operacja oczywiście nigdy nie zakończy się ponieważ czeka ona na na dostęp do wątku głównego blokowanego przez “Task.Result”.

Zachowanie różni się w zależności od typu aplikacji. W przypadku aplikacji konsolowej, nie ma kontekstu synchronizacyjnego (TaskScheduler) więc operacja nie zakończy się deadlock’iem. Jeśli piszemy np. aplikację ASP.NET lub WPF wtedy doświadczymy opisanych wyżej problemów.

Istnieje możliwość wyłączenia mechanizmu powracania do poprzedniego kontekstu. Służy do tego ConfigureAwait(false);

        public async Task<string> FetchDataAsync()
        {
            await DoSomethingAsync().ConfigureAwait(false);

            return "Hello World";
        }

Po wywołaniu ConfigureAwait(false) nie będziemy mieli problemów z deadlock, ponieważ żaden kontekst nie będzie pobierany. Oczywiście nie jest to eleganckie rozwiązanie. ConfigureAwait jest jednak bardzo przydatny ze względów wydajnościowych – np. gdy w pętli wywołujemy await, wtedy zwykle nie ma sensu pobierać kontekstu.

Jeśli zatem wywołujemy metodę async, powinniśmy używać await, inaczej istnieje duże ryzyko, że kod po prostu zawiesi się.

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