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ę.
Dzieki za wpis, nareszcie rozumiem dlaczego mialem deadlocks kiedy uzywalem “.Result”.
Jeszcze by mnie interesowalo jak to jest z metodami asynchronicznymi, ktore zwracaja tylko Task (lub void). Mam wrazenie, ze kiedy je uzywalem to await nie czekal na dokonczenie, ale wykonal nastepujacy kod natychmiast. Zawsze musialem zmienic metody przynajmniej na Task i cos zwracac.
Dalej nie wyjasniles w jaki sposob wywolac metode asynchroniczna jako synchroniczna w sposob bezpieczny. Twoje rozwiazanie zaklada edycje biblioteki (funkcji FetchDataAsync()) do ktorej nie masz dostepu.
Nie widze rozwiazania w twoim poscie, bo w 99,9% przypadkach nie bedziesz w stanie edytowac zewnetrznej biblioteki. Bezpieczne rozwiazanie to stworzenie wlasnego taska i oczekiwanie na result:
string result= Task.Run(FetchDataAsync).Result;
Moim zdaniem, jesli metoda jest typu async, zawsze powinno korzystac sie z await – to bezpieczny sposob.
Jasne, tylko twoj post jest o tym jak wywolac je synchronicznie
@Mistyk ma racje. Brak odpowiedzi na fundamentalne pytanie jak?
Task<IEnumerable> response= Task.Run(async () =>
{
var msg = await _manager.GetInfo();
return msg;
});
//response.Result;
Czy to jest bezpieczne?
Nie jest bezpieczne. Jesli mamy pewnosc, ze nie ma zmiany kontekstu nigdzie po drodze (ConfigureAwait(false)) wtedy jest bezpiecznie, ale nieelegancko – zbyt latwo popelnic blad.
Nie mieszamy await z Response. Nie ma czegos takiego jak “dobre mieszanei” 🙂
Jesli metoda jest await, wtedy do samego konca robimy async\await.