Wraz z wprowadzeniem C# 5.0, pojęcie asynchronicznego programowania stało się dostępne dla szerokiego kręgu programistów. Async to mechanizm, który obiecuje rewolucję w zarządzaniu równoczesnymi zadaniami i poprawie wydajności aplikacji, szczególnie w kontekście operacji, które wymagają oczekiwania, takich jak operacje we/wy. C# 5.0 wprowadza słowa kluczowe async i await, które z jednej strony oferują prostsze podejście do asynchroniczności, z drugiej – mogą wprowadzać pewne zawiłości, których należy być świadomym, aby w pełni wykorzystać ich potencjał.

Czym jest async w C# 5.0?

Async to mechanizm umożliwiający wykonywanie zadań w tle bez blokowania głównego wątku programu. Pozwala na wykonywanie operacji asynchronicznych w sposób, który wydaje się synchroniczny, zachowując przy tym wszystkie korzyści związane z równoczesnym przetwarzaniem. Przy użyciu async możemy oznaczyć metody, które wewnętrznie mogą wykonywać operacje nieblokujące (takie jak oczekiwanie na wynik z sieci, dostęp do bazy danych czy plików), a program w tym czasie może kontynuować inne zadania.

Kluczowym elementem pracy z async jest słowo kluczowe await, które powoduje wstrzymanie wykonywania metody do momentu zakończenia operacji asynchronicznej. Dzięki temu programista nie musi się martwić o zarządzanie wątkami czy synchronizację – wszystko to jest obsługiwane przez środowisko uruchomieniowe.

Zalety asynchronicznego kodu

Dlaczego w ogóle warto sięgnąć po async? Zasadniczo pozwala to na lepsze zarządzanie zasobami, zmniejsza czas oczekiwania użytkownika, a także przyczynia się do poprawy wydajności aplikacji. Gdy mówimy o operacjach we/wy, jak np. odczyt z pliku czy komunikacja z serwerem, użycie async pozwala na wykonanie tych operacji bez blokowania głównego wątku, co skutkuje bardziej responsywnymi aplikacjami. Często aplikacje, które muszą oczekiwać na wynik z zewnętrznych źródeł (np. serwery webowe), mogą wydawać się opóźnione. Dzięki asynchroniczności, czas oczekiwania można zamienić na wykonywanie innych, niezależnych od siebie zadań.

Jednak nie wszystko jest idealne

Choć async oferuje wielką moc, nie zawsze jest rozwiązaniem idealnym. Warto pamiętać, że async nie przyspieszy programu w każdej sytuacji. Dla prostych operacji, które i tak szybko kończą się na jednym wątku, jego zastosowanie może okazać się niepotrzebnym obciążeniem. Istnieją także przypadki, gdzie złożoność związana z asynchronicznymi metodami może prowadzić do błędów, szczególnie w kontekście zarządzania wyjątkami, stanem metody czy synchronizacją.

W szczególności, jeśli w aplikacji zachodzi konieczność pracy z wieloma współbieżnymi operacjami lub wymagany jest ścisły porządek operacji (np. w grach czy w systemach czasu rzeczywistego), asynchroniczność może wprowadzać komplikacje w zarządzaniu logiką aplikacji. Czasami tradycyjne, manualne zarządzanie równoczesnością lub blokowanie wątków może okazać się bardziej przewidywalne i prostsze.

Czy każdą metodę warto oznaczać jako async?

Jednym z głównych błędów, który pojawia się przy wprowadzaniu asynchronicznych metod, jest nadmierne oznaczanie metod jako async, gdy nie ma to rzeczywistego sensu. Oznaczanie każdej metody jako asynchronicznej może prowadzić do spadku wydajności, a także komplikować strukturę programu, czyniąc ją trudniejszą do zarządzania i testowania. Ważne jest, aby rozpoznać, które operacje rzeczywiście mogą skorzystać na asynchroniczności. W sytuacji, gdy operacje są szybkie, lub nie wymagają oczekiwania na zewnętrzne zasoby, użycie async może wprowadzać zbędną złożoność.

Współpraca z innymi technologiami

Async w C# doskonale współpracuje z innymi nowoczesnymi technologiami, takimi jak ASP.NET, gdzie może znacząco poprawić wydajność aplikacji webowych, umożliwiając efektywne zarządzanie wieloma równocześnie działającymi zapytaniami użytkowników. Dla przykładu, w aplikacjach ASP.NET MVC 4, użycie asynchronicznych metod pozwala na lepsze skalowanie serwerów, zwłaszcza w przypadku aplikacji o dużym natężeniu ruchu.

W kontekście aplikacji mobilnych (np. Windows Phone, Silverlight), asynchroniczność jest wręcz niezbędna, aby zapewnić użytkownikowi płynne działanie aplikacji, jednocześnie wykonując operacje w tle.

Równoczesność i synchronizacja

Choć async pozwala na równoczesne wykonywanie wielu operacji, nie oznacza to, że każda operacja asynchroniczna jest wykonywana na osobnym wątku. W rzeczywistości, w tle mogą być używane te same wątki, a równoczesność jest realizowana poprzez harmonogramowanie zadań, które mogą być zawieszone i wznowione bez blokowania innych operacji. Warto jednak pamiętać, że synchronizacja kontekstu (np. w aplikacjach desktopowych) może prowadzić do komplikacji, jeśli nie będziemy odpowiednio zarządzać wątkami lub nie rozpoznamy, kiedy operacja asynchroniczna wymaga wrócenia na główny wątek aplikacji.

Podsumowanie

Właściwe zrozumienie i zastosowanie async w C# 5.0 to umiejętność, która może znacząco poprawić jakość i wydajność aplikacji. Kluczem do sukcesu jest jednak umiejętność rozpoznania, kiedy asynchroniczność jest rzeczywiście potrzebna, a także odpowiednia synchronizacja i zarządzanie wyjątkami, które mogą wystąpić w złożonych scenariuszach. Należy także pamiętać, że nie każda metoda wymaga stosowania async, a nadmierne jej użycie może prowadzić do niepotrzebnej złożoności w kodzie.

Jak obsługiwać wyjątki w kodzie asynchronicznym: kluczowe zasady i najlepsze praktyki

W świecie programowania asynchronicznego istnieje kilka zasadniczych różnic w porównaniu do tradycyjnego, synchronicznego podejścia, szczególnie jeśli chodzi o obsługę wyjątków. Jednym z najważniejszych aspektów jest to, gdzie i kiedy wyjątek zostaje zgłoszony w metodzie asynchronicznej. W metodach asynchronicznych wyjątek jest zgłaszany w momencie wywołania await, a nie bezpośrednio w metodzie. Jest to szczególnie istotne, gdy rozdzielamy samo wywołanie od oczekiwania na zakończenie operacji.

Przykład:

csharp
// To nigdy nie zgłasza wyjątku
Task task = Thrower(); try { await task; } catch (AlexsException) { // Zdarzenie dotrze tutaj }

W takich przypadkach łatwo jest zapomnieć o wywołaniu await w metodzie asynchronicznej, szczególnie jeśli metoda zwraca Task bez wartości zwracanej. W takim przypadku jest to równoznaczne z ignorowaniem wszystkich wyjątków i ich cichym przechwytywaniem. Zdecydowanie jest to zła praktyka, ponieważ prowadzi do nieprawidłowego stanu programu oraz trudnych do wykrycia błędów, które mogą pojawić się daleko od miejsca ich przyczyny. Dlatego warto zawsze pamiętać, by oczekiwać na wykonanie każdej metody asynchronicznej, aby uniknąć niepotrzebnego debugowania.

Należy pamiętać, że to zachowanie, które ignoruje wyjątki, jest nowością w wersjach .NET po wprowadzeniu programowania asynchronicznego. W starszych wersjach, wyjątkowe sytuacje były obsługiwane inaczej, a wyjątki z kodu biblioteki Task Parallel Library (TPL) były ponownie rzucane na wątku finalizującym. W .NET 4.5 i późniejszych wersjach to zachowanie zostało zmienione.

Wyjątki w metodach asynchronicznych zwracających void

Metody asynchroniczne, które zwracają void, nie mogą być oczekiwane za pomocą await, a ich obsługa wyjątków wymaga innego podejścia. Zdarza się, że nie chcemy, aby wyjątki te były ignorowane, ale dla metod asynchronicznych typu void sytuacja jest bardziej skomplikowana. Każdy wyjątek, który opuści metodę asynchroniczną typu void, zostanie ponownie zgłoszony na wątku wywołującym:

  • Jeśli przy wywołaniu metody istniał SynchronizationContext, wyjątek zostanie przekazany do niego.

  • W przeciwnym razie wyjątek zostanie zgłoszony na wątku puli wątków.

W większości przypadków te wyjątki doprowadzą do zakończenia procesu, chyba że odpowiedni handler dla nieobsłużonych wyjątków został przypisany do właściwego zdarzenia. Z tego powodu warto tworzyć metodę asynchroniczną typu void tylko wtedy, gdy jest to absolutnie konieczne (np. dla kodu wywołującego zewnętrzną bibliotekę) lub kiedy mamy pewność, że metoda nie wyrzuci wyjątku.

Mechanizm „Fire and Forget”

W rzadkich przypadkach, gdy nie interesuje nas, czy metoda zakończy się powodzeniem, a oczekiwanie na jej wykonanie byłoby skomplikowane, istnieje możliwość, by nie czekać na jej zakończenie. Moja sugestia w takim przypadku to nadal zwrócenie Task, ale przekazanie go do metody, która zajmie się obsługą wyjątków:

csharp
public static void ForgetSafely(this Task task)
{ task.ContinueWith(HandleException); }

Metoda HandleException mogłaby np. zapisywać wyjątek w systemie logowania. Jest to podejście, które pozwala na skuteczną obsługę potencjalnych problemów bez zatrzymywania głównego przepływu aplikacji.

Wyjątki w metodach typu Task.WhenAll

W środowisku asynchronicznym, różnica polega na tym, że możemy napotkać sytuację, która w świecie synchronicznym była niemożliwa – metoda może zgłosić wiele wyjątków jednocześnie. Na przykład, podczas używania Task.WhenAll, który oczekuje zakończenia grupy operacji asynchronicznych, wiele z nich może zakończyć się błędem. Każdy wyjątek w takim przypadku nie będzie wyrzucany jako pierwszy ani najistotniejszy.

WhenAll jest mechanizmem, który może generować wiele wyjątków jednocześnie. W takich przypadkach, Task zawiera wyjątek AggregateException, który przechowuje listę innych wyjątków. Zwykle wyjątek, który ucieka z metody asynchronicznej, będzie zawarty w AggregateException jako wewnętrzny wyjątek, ale w przypadku WhenAll może on zawierać więcej niż jeden.

Przykład:

csharp
Task allTask = Task.WhenAll(tasks);
try { await allTask; } catch { foreach (Exception ex in allTask.Exception.InnerExceptions) { // Zrób coś z wyjątkiem } }

Dzięki takiej obsłudze możemy odzyskać pełną listę wyjątków, co jest szczególnie istotne w przypadku wielu asynchronicznych operacji, które kończą się błędami.

Rzucanie wyjątków synchronicznie

Zalecenia TAP (Task-based Asynchronous Pattern) pozwalają na synchronizację metod rzucających wyjątki, ale tylko w przypadku, gdy wyjątek oznacza błąd w wywołaniu metody, a nie błąd napotkany podczas jej wykonania. Jeśli chcesz, by wyjątek był rzucany synchronicznie, musisz wykonać mały trik, używając metody synchronicznej, która sprawdza warunki przed wywołaniem metody asynchronicznej:

csharp
private Task GetFaviconAsync(string domain) {
if (domain == null) throw new ArgumentNullException("domain"); return GetFaviconAsyncInternal(domain); }

Taki sposób pozwala na lepsze śledzenie stosu wywołań i ułatwia interpretację błędów.

Blok finally w metodach asynchronicznych

Warto wiedzieć, że w metodach asynchronicznych możemy używać bloku finally, który działa podobnie jak w metodach synchronicznych. Blok finally zostanie zawsze wykonany, niezależnie od tego, czy metoda zakończy się poprawnie, czy wystąpi wyjątek. Warto jednak pamiętać, że w metodach asynchronicznych nie ma gwarancji, że wykonanie opuści metodę w ogóle. Metodę asynchroniczną można napisać w taki sposób, że po wywołaniu await zostanie zapomniana i usunięta przez garbage collector:

csharp
async void AlexsMethod() {
try { await DelayForever(); } finally { // Nigdy się nie wydarzy } }

Dodatkowe wskazówki

Warto zrozumieć, że obsługa wyjątków w metodach asynchronicznych wymaga nieco innego podejścia, niż w przypadku tradycyjnych metod synchronicznych. Należy unikać "zapomnianych" metod asynchronicznych, które nie są oczekiwane, oraz pamiętać o tym, by zawsze dbać o odpowiednią obsługę wyjątków, szczególnie w przypadku, gdy wykonanie operacji nie jest krytyczne dla dalszego działania programu. Sugeruje się również, by metody asynchroniczne typu void były używane jedynie w przypadkach, gdy jest to naprawdę konieczne, i nigdy bez odpowiedniej kontroli błędów.

Jak programować równolegle z użyciem async i aktorów?

Programowanie asynchroniczne i równoległe stało się kluczową częścią współczesnego tworzenia oprogramowania, umożliwiając efektywne wykorzystanie zasobów sprzętowych i zapewniając lepszą skalowalność. Istnieje wiele podejść do tego typu programowania, jednak jedno z najczęściej używanych to wykorzystanie mechanizmu "await" oraz struktury aktorów. Dla programistów C# rozumienie tych narzędzi jest kluczowe, by optymalizować wydajność aplikacji, szczególnie gdy mamy do czynienia z operacjami wymagającymi długotrwałych obliczeń lub operacji sieciowych.

Podstawową zasadą w programowaniu asynchronicznym jest to, że zasoby mogą zostać zwolnione podczas oczekiwania na zakończenie operacji. Oznacza to, że zamiast trzymać blokady na danych lub zasobach w trakcie oczekiwania, kod może być przerywany, pozwalając innym operacjom na wykonanie swoich zadań. Jednakże, jak pokazuje przykład z użyciem słowa kluczowego „await”, należy unikać rezerwowania zasobów w tym czasie, aby nie doszło do nieoczekiwanych konfliktów w kodzie. Przykład pokazuje, że najlepszym rozwiązaniem jest unikanie blokowania kodu przy pomocy "lock", gdy w programie wykorzystujemy asynchroniczność.

Wyobraźmy sobie sytuację, w której nasza aplikacja czeka na wynik operacji sieciowej, np. po naciśnięciu przez użytkownika przycisku w interfejsie. Jeśli w tym czasie użytkownik naciśnie inny przycisk, aplikacja powinna odpowiednio zareagować. Właśnie na tym polega siła programowania asynchronicznego w interfejsach użytkownika: umożliwia utrzymanie responsywności aplikacji, nawet gdy część kodu jest w stanie oczekiwania. Należy jednak pamiętać, że po wznowieniu działania programu po „await” może zajść zmiana stanu w aplikacji, co wymaga czasem drugiej, pozornie zbędnej weryfikacji, czy nadal możemy kontynuować działanie, jak wcześniej zakładano.

W kontekście UI warto zauważyć, że chociaż w aplikacjach jest tylko jeden wątek interfejsu użytkownika, stanowi on swoisty „lock” w sensie, że tylko ten wątek ma dostęp do zasobów UI. Tylko wątek interfejsu użytkownika może manipulować danymi wyświetlanymi na ekranie, co sprawia, że kod interfejsu użytkownika jest z natury bezpieczny, pod warunkiem, że pamiętamy o odpowiednim rozmieszczeniu wywołań „await”. Główne niebezpieczeństwo pojawia się, gdy operacje sieciowe lub obliczeniowe są rozdzielane w czasie, a program nie jest przygotowany na zmianę stanu w trakcie oczekiwania na ich zakończenie.

Również warto przyjrzeć się koncepcji aktorów, która stanowi zaawansowaną metodę tworzenia aplikacji równoległych. W podejściu aktorów, jednostki obliczeniowe (aktorzy) są odpowiedzialne za konkretne dane i nie dzielą ich z innymi aktorami. Dzięki temu programowanie równoległe staje się bardziej skalowalne, a błędy związane z wyścigami danych (race conditions) czy martwymi blokadami (deadlocks) są łatwiejsze do uniknięcia. W przypadku aktorów, w przeciwieństwie do tradycyjnych blokad, jeden wątek nigdy nie ma dostępu do wielu aktorów jednocześnie. W takiej sytuacji, jeśli dany wątek chce wykonać zadanie w innym akcie, musi wykonać wywołanie asynchroniczne i pozwolić innym aktorom na równoległe działanie. W ten sposób każde zadanie jest wykonywane na własnym wątku i nie ma ryzyka kolizji z innymi procesami.

Aktorzy są również bardziej efektywni niż tradycyjne podejście z użyciem współdzielonej pamięci, co staje się coraz mniej adekwatne do współczesnych architektur komputerowych, zwłaszcza w kontekście rozproszonego przetwarzania równoległego. Warto zauważyć, że choć programowanie aktorów może wydawać się podobne do korzystania z blokad, to jednak różni się od niego w kwestii wykorzystania wątków i zarządzania stanem danych, umożliwiając znacznie lepszą skalowalność w większych systemach.

W języku C# możemy zaimplementować model aktorów z użyciem bibliotek, takich jak NAct, które umożliwiają przekształcenie zwykłych obiektów w aktorów. Na przykładzie usługi generowania liczb pseudolosowych w kontekście szyfrowania strumienia danych, widać, jak łatwo można wykorzystać aktorów do równoległego przetwarzania zadań, takich jak generowanie liczb i szyfrowanie danych. Zastosowanie NAct pozwala na automatyczne rozdzielenie zadań na różne wątki, co znacząco poprawia wydajność programu. Generowanie liczb pseudolosowych jest operacją, która często wymaga dużych zasobów obliczeniowych, a wykorzystanie aktorów umożliwia jej równoległe wykonanie bez ryzyka kolizji w dostępie do danych.

Dodatkowym narzędziem, które warto poznać, jest TPL Dataflow – biblioteka, która wspiera programowanie opierające się na przepływie danych. Dzięki niej możemy zdefiniować ciąg operacji, które mają zostać wykonane na danych wejściowych, a system automatycznie zdecyduje, które z nich mogą zostać równolegle wykonane. Takie podejście jest szczególnie użyteczne w przypadku problemów związanych z transformacją danych, gdzie obliczenia są jednoczesne i niezależne od siebie. TPL Dataflow działa na zasadzie przesyłania wiadomości pomiędzy blokami, a każda operacja na danych jest realizowana asynchronicznie, zapewniając równoległe wykonanie tych zadań.

Programowanie równoległe z wykorzystaniem asynchroniczności i aktorów pozwala na lepsze wykorzystanie zasobów komputerowych i poprawę wydajności aplikacji, jednocześnie upraszczając kod i minimalizując ryzyko wystąpienia problemów związanych z blokadami czy dostępem do współdzielonej pamięci. Warto jednak pamiętać, że w przypadku korzystania z takich narzędzi jak NAct czy TPL Dataflow, kluczowym elementem pozostaje odpowiednie zaplanowanie punktów oczekiwania, by uniknąć niepożądanych efektów ubocznych, które mogą pojawić się w wyniku równoległego przetwarzania zadań.

Jak używać asynchronicznego programowania w C# 5.0: Wprowadzenie do najnowszych technik

W programowaniu asynchronicznym kod wykonuje operację długotrwałą, ale nie czeka na jej zakończenie. Zamiast tego, kontynuuje inne zadania, podczas gdy operacja jest realizowana w tle. Tego rodzaju podejście stanowi przeciwieństwo kodu blokującego, który zatrzymuje całą aplikację do momentu zakończenia operacji. Asynchroniczność ma ogromne znaczenie w wielu nowoczesnych aplikacjach, szczególnie w kontekście rozwoju oprogramowania, które wymaga nieprzerwanego działania oraz wysokiej wydajności.

Ważne jest zrozumienie, że asynchroniczność polega na tym, iż operacje, które normalnie mogłyby blokować wątek, odbywają się w sposób nieblokujący. Kiedy w programie występują długotrwałe operacje, jak na przykład dostęp do sieci, dysku lub opóźnienia związane z czasem oczekiwania, asynchroniczne programowanie pozwala na kontynuowanie innych działań bez konieczności czekania na zakończenie tej operacji.

Aby lepiej wyjaśnić tę zasadę, warto przyjrzeć się roli wątku w programie. Każdy kod w większości języków programowania działa w ramach wątku systemowego, który jest zarządzany przez system operacyjny. W przypadku kodu blokującego, wątek jest "zajęty" i nie może wykonywać innych operacji, dopóki aktualnie realizowana czynność się nie zakończy. Z kolei w przypadku kodu asynchronicznego, wątek nie pozostaje w stanie "czekania", lecz przełącza się na inne zadania, umożliwiając wykonanie innych operacji w trakcie oczekiwania na wynik długotrwałej czynności.

W codziennej pracy programisty, asynchroniczność jest często wykorzystywana w różnych kontekstach. Może to być obsługa zapytań sieciowych, odczyt danych z dysku, operacje na bazach danych, a także wszelkie zadania wymagające interakcji z użytkownikiem, które nie mogą blokować reszty aplikacji. Aplikacje webowe, które oferują użytkownikowi możliwość wykonywania kilku działań jednocześnie, także opierają się na asynchronicznych mechanizmach.

Jednym z głównych powodów, dla których warto stosować podejście asynchroniczne, jest poprawa wydajności i responsywności aplikacji. Zamiast czekać na zakończenie jednej operacji, program może przejść do następnej, co szczególnie ma znaczenie w aplikacjach działających w czasie rzeczywistym lub obsługujących dużą liczbę użytkowników. Na przykład, w przypadku aplikacji internetowej, serwer, który obsługuje wiele zapytań, korzysta z asynchroniczności, aby jedno zapytanie nie blokowało innych. W kontekście aplikacji mobilnych i desktopowych, asynchroniczność pozwala na płynniejsze i bardziej responsywne interfejsy użytkownika.

C# 5.0 wprowadza nowe mechanizmy, które ułatwiają implementację asynchronicznych operacji, w tym słowo kluczowe async oraz await. Dzięki tym elementom kod staje się bardziej przejrzysty, a programista nie musi zarządzać ręcznie wątkami czy innymi bardziej skomplikowanymi operacjami synchronizacji. Jednak mimo to, zastosowanie asynchronicznych technik wymaga znajomości pewnych zasad i ograniczeń, które można napotkać w trakcie programowania.

Jednym z kluczowych wyzwań, z którymi spotykają się programiści, jest zarządzanie stanem aplikacji w przypadku asynchronicznych operacji. W tradycyjnym, blokującym kodzie stan aplikacji jest stosunkowo łatwy do zarządzania, ponieważ program wykonuje zadania sekwencyjnie. W przypadku asynchroniczności, operacje mogą zakończyć się w różnym czasie, co może prowadzić do problemów z synchronizacją stanu aplikacji. Aby uniknąć błędów, niezbędne jest odpowiednie zarządzanie kontekstem wykonania i unikanie niepożądanych interakcji między różnymi zadaniami.

Istotnym elementem asynchroniczności w C# jest także możliwość korzystania z bibliotek i frameworków, które wspierają ten model, takich jak ASP.NET czy WinRT. Oba te środowiska w pełni wykorzystują zalety asynchronicznych operacji w kontekście aplikacji webowych oraz aplikacji dla systemu Windows, umożliwiając ich skalowanie i zwiększając ich wydajność.

Zrozumienie, kiedy i jak stosować asynchroniczne operacje, jest kluczowe, aby móc wykorzystać pełen potencjał tej technologii. Z jednej strony, jej zastosowanie pozwala na poprawę responsywności aplikacji, a z drugiej strony może prowadzić do trudności w zarządzaniu skomplikowanymi scenariuszami, zwłaszcza jeśli kod jest niewłaściwie zorganizowany. Warto zwrócić uwagę na możliwość tworzenia bardziej modularnych aplikacji, gdzie poszczególne operacje są odseparowane i mogą być wykonywane równolegle, co znacząco poprawia wydajność systemu.

Rozważając implementację asynchroniczności, należy także pamiętać o jej wpływie na testowanie aplikacji. Asynchroniczny kod może być trudniejszy do testowania, szczególnie gdy operacje są skomplikowane lub wymagają synchronizacji z innymi częściami systemu. Dlatego w procesie tworzenia aplikacji warto uwzględnić odpowiednie mechanizmy testowania, które pomogą wychwycić potencjalne błędy związane z błędną synchronizacją lub problemami z wydajnością.

W kontekście C# 5.0 asynchroniczność to narzędzie, które, stosowane w odpowiednich przypadkach, może znacznie poprawić działanie aplikacji, zwłaszcza w systemach wymagających wysokiej wydajności i responsywności. Jednak każda technika programistyczna wiąże się z pewnymi wyzwaniami, których świadomość pozwala na skuteczniejsze i bardziej efektywne jej wykorzystanie.

Jak przekształcić przykład użycia Favicon na wersję asynchroniczną w C#

Zmodyfikujemy teraz poprzedni przykład użycia favicon w przeglądarce, aby wykorzystać asynchroniczność. Jeśli masz dostęp do oryginalnej wersji tego przykładu (domyślna gałąź), spróbuj zmienić ją, dodając słowa kluczowe async i await przed kontynuowaniem lektury. Ważną metodą w tym przykładzie jest AddAFavicon, która pobiera ikonę, a następnie dodaje ją do interfejsu użytkownika. Naszym celem jest uczynienie tej metody asynchroniczną, aby wątek interfejsu użytkownika mógł swobodnie reagować na działania użytkownika podczas pobierania pliku.

Pierwszym krokiem jest dodanie słowa kluczowego async do sygnatury metody. Jest to rozwiązanie analogiczne do użycia słowa kluczowego static. Następnie, w celu poczekania na pobranie danych, używamy słowa kluczowego await. W składni C# await działa jak operator unarny, podobnie jak operator ! (negacja) lub operator rzutowania (typ). Operator await jest stosowany po lewej stronie wyrażenia, co oznacza, że program będzie czekał na jego wynik w sposób asynchroniczny.

Ostatecznie należy zmienić wywołanie DownloadData na asynchroniczną wersję metody, DownloadDataTaskAsync. Warto zauważyć, że sama metoda asynchroniczna nie jest automatycznie asynchroniczna w sensie całkowitej implementacji. Metody async ułatwiają jedynie korzystanie z innych metod asynchronicznych. Zaczynają one działać synchronicznie, aż wywołają asynchroniczną metodę i zaczną na nią czekać. Wówczas same stają się asynchroniczne. Czasami zdarza się, że metoda async nigdy nie czeka na żadną operację, wówczas działa synchronicznie.

csharp
private async void AddAFavicon(string domain) {
WebClient webClient = new WebClient(); byte[] bytes = await webClient.DownloadDataTaskAsync("http://" + domain + "/favicon.ico"); Image imageControl = MakeImageControl(bytes); m_WrapPanel.Children.Add(imageControl); }

Porównując to z innymi wersjami kodu, które rozważaliśmy, ta wersja wygląda podobnie do pierwotnej wersji synchronicznej. Nie wprowadzono nowych metod, tylko kilka drobnych modyfikacji w strukturze kodu. Jednakże, w działaniu, jest to wersja asynchroniczna, podobna do tej, którą przedstawiliśmy wcześniej w rozdziale o ręcznej asynchroniczności.

Task i await

Rozważmy teraz wyrażenie await, które użyliśmy w powyższym przykładzie. Oto sygnatura metody WebClient.DownloadStringTaskAsync:

csharp
Task DownloadStringTaskAsync(string address)

Typ zwracany przez tę metodę to Task. Jak wspomniano we wcześniejszym rozdziale, Task reprezentuje operację, która trwa w czasie i zakończy się wynikiem w przyszłości. Można to porównać do obietnicy zwrócenia wartości typu T, gdy operacja zakończy się. Task i Task<T> są używane do reprezentowania operacji asynchronicznych i obydwa umożliwiają przekazanie kodu do wykonania po zakończeniu operacji. Aby wykorzystać tę możliwość ręcznie, używamy metody ContinueWith, która przyjmuje delegat, zawierający kod, który ma zostać wykonany po zakończeniu długotrwałej operacji.

Operator await wykorzystuje tę samą funkcjonalność do wykonywania reszty metody async w ten sam sposób. Kiedy zastosujemy await do obiektu typu Task, wyrażenie await zostaje zamienione na typ T. Oznacza to, że wynik operacji asynchronicznej może zostać przypisany do zmiennej i wykorzystany w dalszej części metody. Jeśli natomiast await odnosi się do niegenerycznego Task, wyrażenie staje się po prostu instrukcją, której wynik nie może zostać przypisany do zmiennej.

csharp
await smtpClient.SendMailAsync(mailMessage);

Należy zauważyć, że nic nie stoi na przeszkodzie, by rozdzielić wyrażenie await, aby uzyskać dostęp do obiektu Task bez oczekiwania na jego zakończenie, lub wykonać inne operacje przed wywołaniem await.

csharp
Task myTask = webClient.DownloadStringTaskAsync(uri);
// Wykonaj coś tutaj string page = await myTask;

Istotne jest zrozumienie, że metoda DownloadStringTaskAsync zaczyna wykonywać się natychmiast po jej wywołaniu. Rozpoczyna ona działanie synchronicznie w bieżącym wątku, a po rozpoczęciu pobierania danych zwraca obiekt typu Task, nadal wykonując operację w tym samym wątku. Dopiero po zastosowaniu await do tego obiektu, kompilator przetwarza operację w sposób asynchroniczny. Oznacza to, że sama operacja długoterminowa rozpoczyna się od wywołania DownloadStringTaskAsync, co pozwala na równoczesne wykonywanie wielu operacji asynchronicznych.

csharp
Task firstTask = webClient1.DownloadStringTaskAsync("http://oreilly.com"); Task secondTask = webClient2.DownloadStringTaskAsync("http://simple-talk.com"); string firstPage = await firstTask; string secondPage = await secondTask;

Jest to jednak niebezpieczne podejście, jeśli obie operacje mogą rzucić wyjątki. Jeśli oba zadania rzucą wyjątek, pierwszy await rozpropaguje wyjątek, co oznacza, że drugie zadanie nigdy nie zostanie oczekiwane. Jego wyjątek nie będzie obserwowany, co może prowadzić do utraty wyjątku lub nawet jego ponownego wystąpienia na nieoczekiwanym wątku, co może zakończyć proces. W rozdziale 7 przedstawimy bardziej odpowiednie techniki obsługi takich przypadków.

Typy zwracane przez metody asynchroniczne

Metody oznaczone słowem kluczowym async mogą zwracać trzy główne typy:

  • void

  • Task

  • Task<T>

Inne typy zwracane nie są dozwolone, ponieważ metody asynchroniczne zazwyczaj nie kończą się w momencie swojego zwrócenia. Zwykle metoda async oczekuje na operację długotrwałą, co sprawia, że metoda zwraca szybko, ale wynik będzie dostępny dopiero później. Istnieje różnica między typem zwracanym przez metodę, a typem wyniku, który programista chce uzyskać. Na przykład, Task reprezentuje operację, ale nie zawiera wyniku.

Typ zwracany void jest odpowiedni tylko wtedy, gdy wywołujący metodę nie potrzebuje oczekiwać na jej zakończenie ani poznać jej statusu. Metody async void są stosowane bardzo rzadko, zazwyczaj w miejscach, gdzie interfejs użytkownika wymaga tego typu powrotu (np. w obsłudze zdarzeń UI). Częściej stosuje się metody zwracające Task, które umożliwiają oczekiwanie na zakończenie operacji i obsługę wyjątków. Jeśli operacja asynchroniczna ma zwrócić wartość, używamy Task<T>, gdzie T to typ wynikowy.

Podpisy metod asynchronicznych i interfejsy

Słowo kluczowe async pojawia się w deklaracji metody, podobnie jak inne słowa kluczowe, takie jak public czy static. Warto jednak pamiętać, że async nie jest częścią podpisu metody w kontekście nadpisywania innych metod czy implementowania interfejsów. Słowo kluczowe async wpływa jedynie na kompilację metody, a nie na to, jak metoda będzie wchodziła w interakcje z innymi elementami systemu.

csharp
class BaseClass {
public virtual async Task AlexsMethod() { ... } } class SubClass : BaseClass {
public override async Task AlexsMethod() {
... } }