WPF AsyncAsync i Await to całkiem nowy fantastyczny bajer, który został dodany do języka C# całkiem nie dawno. Można go używać w każdej aplikacji .NET-owej, chociaż małe różnice w wykorzystaniu istnieją to sumie filozofia działania jest ta sama .
Ciężko jest wyjaśnić co dokładnie dzieje się pod spodem aplikacji, która korzysta z tej funkcjonalności.
Celem tego wpisu jest pokazanie tobie jak łatwo z tej funkcjonalności skorzystać bez zbędnego gadania.
Operacje pobierania danych z sieci czy operacje dyskowe zajmują na tyle dużo czasu ,aby zablokować wątek główny aplikacji (wątek UI). Jeśli, więc napiszemy taką aplikację desktopową bez żadnej asynchroniczności, to mniej więcej będziesz mógł zaobserwować poniższe zachowanie.
private void Button_Click_1(object sender, RoutedEventArgs e)
{
int number1 = int.Parse(txtNumber1.Text);
int number2 = int.Parse(txtNumber2.Text);
int result = 0;
Thread.Sleep(4000);
result = number1 + number2;
MessageBox.Show(result.ToString());
}
Powyższa aplikacja dodaje dwie liczby. Proste nieprawdaż problem polega na tym ,że obliczenie tej wartości zajmuje jej 4 sekundy. Przez te 4 sekundy aplikacja totalnie zamarza. Co można z tym zrobić.
Gdy ja byłem studentem do odblokowania wątku głównego używałem backgroundWorker-a. Istnieje wiele sposobów do wykonania tego zdania. . Oczywiście ja skorzystam z rozwiązania polegających na użyciu nowych słów kluczowych Async i Await. Brew pozorom te nowe rozwiązanie wymaga najmniejszej ilości kodu.
Aplikacje Metro/ModerUI wykorzystują namiętnie tą funkcjonalności ze względu na to ,że płynności aplikacji na urządzenia mobilny gra naprawdę dużą rolę w doświadczeniu użytkownika.
Nawet rozwiązanie polegające na tworzeniu instancji klasy Task<T> z .NET 4.0 się przy tym chowa. W końcu nie chcemy tego całego bałaganu polegającego na rozbijaniu asynchronicznych czynności na wiele metod (Begin,End).
W .NET 4.5 klasa Task reprezentuje "przyszłość". Jeśli nie zapoznałeś się z działem klasy Task<T> z .NET 4.0 to nawet lepiej, ponieważ mógłbyś się poczuć teraz trochę zakłopotany,
Skoro klasa Task<T> reprezentuje wynik, który pojawi się przyszłości to dlaczego by nie stworzyć metody, która będzie zwracać "przyszłość". W tym wypadku po 4 sekundach chce zwrócić wartość liczby całkowitej (int) dlatego moja metoda "Calculate" zwraca Task<int>. Jeśli metoda miałaby, nic nie zwraca (typ void) wtedy korzystamy z niegenerycznej wersji klasy Task po prostu "Task".
private async Task<int> Calculate(int number1, int number2)
{
return await Task.Run(() =>
{
Thread.Sleep(4000);
return number1 + number2;
});
}
Co robią słowa kluczowe async i await. Mniej więcej to samo co wcześniej. Kod po kompilacji wygląda, mniej więcej tak samo jakby się używało klasycznych metod wywołania zwrotnego (callback). Oczywiście rozwiązanie async i await jest wygodne, bo dla oka wydaje się ,że metoda działa synchronicznie .Jak linijka po linijce.
Co na to Visual Studio 2012. Visual Studio mówi ,że metoda Calculate jest "awaitable", czyli "poczekalska" .Poza tym ,że nie ma takiego słowa w języku polskim "co to w ogóle znaczy".
Aby metoda stała się "awaitable" musi ona tylko spełniać kilka warunków.
- Musi ona zawracać instancje klasy Task<T> bądź Task. Przynajmniej tak to wygląda. W rzeczywistość jest zwracana instancja klasy TaskAwaiter<T> . Metody wewnątrz tej klasy nie są fascynującą lekturą. Naprawdę lepiej jest powiedzieć ,że metoda zwraca już odpalone zadanie albo "przyszłość".
- Musi być oznaczona słowem async. Bynajmniej to wszystko, co to słowo kluczowe robi. Oznacza to ,że w którymś miejscu w metodzie zostanie użyte słowo "await".
- Słowo Async może być użyte tylko dla metod zwracających "void" (zdarzenia) lub Task<T>.
- Musi zwracać zadanie ze słowem kluczowym "await". Jest to symbol miejsca, gdzie kod jest rozbijany na części.
Co robi słowo kluczowe "await". W sumie informuje on kompilator ,że dane zadanie musi być właśnie w tym momencie uruchomione.
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
int number1 = int.Parse(txtNumber1.Text);
int number2 = int.Parse(txtNumber2.Text);
int result = await Calculate(number1, number2);
MessageBox.Show(result.ToString(CultureInfo.InvariantCulture));
}
Kod wygląda synchronicznie ,ale tak nie jest. Kod działa tak.
Teraz wszystko jasne nieprawdaż. Klikając na przycisk pobierasz zawartości pól tekstowych ,a po wywołaniu metody Calcaulate działanie tego zdarzenie w sumie się skończyło. W tle działa metoda Calcaute ,ale nie blokuje ona wątku UI, ponieważ wyszliśmy już ze zdarzenia "Click".
Gdy zadanie zostanie zakończone wydaje się nam ,że w ogóle nie wyszliśmy ,że zdarzenia Click jednak w rzeczywistość jest to iluzja kompilacji. Metoda ta linijka po linijce nie działała synchronicznie.
Istnieje też bardzo ważny aspekt działania metody Async i Await. Mam nadzieje ,że pisząc ten artykuł nigdzie, nie użyłem słowa inny wątek bądź nowy wątek. Mechanizm Async i Await nie są wielowątkowe przynajmniej nie zawsze . W tym wypadku użyłem Task.Run więc nowy wątek powstał.
Wszystko jednak może się odbywać bez tworzenia dodatkowych wątków i być równocześnie asynchroniczne. Ogarnij to!
Stworzenie nowego wątku zajmuje przynajmniej 0,5/1 MB pamięci. Wyobraź sobie ,że aplikację Metro/Moder UI w Windows 8 bez przerwy wykonują operacje oparte na async i await. Trochę głupio by było, gdyby każda operacja w Windows 8 tworzyła nowy watek. Nie byłoby to wydajne i wymagałoby to od programisty używania tej funkcjonalności z głową…ale skoro tak nie jest, nie ma się czym martwić.
Jak więc przy jednym wątku występuje asynchroniczność? W wielkim skrócie aplikacja w swojej głównej pętli skanuje, co się dzieje i równolegle sprawdza i wykonuje, co trzeba.
Równolegle to złe słowo. Słowo współbieżność* lepiej tutaj pasuje.
Można to nazwać asynchronicznym czekaniem. No dobra ,ale gdzie wykonuje się operacja liczenia. Wątek UI nie zawiesił się ,a zarazem w teorii nie musimy tworzyć nowego wątku. Oprócz wątku UI mamy także wątek IOCP.
Wątek UI i IOCP jest już stworzony na potrzeby systemu operacyjnego.
Na tym chciałbym skończyć swój wpis. Dalsze wpisy o async i await wymagają głębszej analizy. Chcę mieć pewność ,że nie piszę jakichś głupot.
Edit z 2022: Kod umieściłem na GitHubie : PanNiebieski/AsyncAwaitWPF (github.com)