Asynchroniczność w ASP.NET Core

16 stycznia 2020 o 21:17 Autor:

Programowanie asynchroniczne jest tematem, który w ostatnim czasie bardzo mocno zyskał na popularności. Dzieje się tak, moim zdaniem, z kilku powodów. Przede wszystkim sprzęt, na którym pracujemy może coraz więcej zadań wykonywać równolegle. Do tego aplikacje, które tworzymy muszą przyjmować coraz większy ruch. Dodatkowo same języki programowania zaczynają nam w tym wszystkim pomagać.

Czym jest asynchroniczność?

Asynchroniczność, o której rozmawiamy można określić jako możliwość wykonywania zadań nielinearnie. Czyli można wykonywać kolejne zadanie zanim zakończy się poprzednie.

Można to porównać do dwóch sposobów komunikacji pomiędzy osobami (w przypadku systemów kolejkowych taka analogia będzie jeszcze bliższa prawdy bo tam mamy faktycznie komunikację dwóch „podmiotów”). Chodzi mianowicie o telefon i email.

Komunikacja synchroniczna

Telefon reprezentuje komunikację synchroniczną. Nie możemy zacząć „nadawać wiadomości” dopóki druga strona nie podniesie słuchawki. Więc jesteśmy zablokowani do czasu otrzymania odpowiedzi. Dodatkowo kiedy my mówimy to druga strona musi czekać aż skończymy, żeby móc odpowiedzieć (przynajmniej jako kulturalni ludzie tak się zazwyczaj zachowujemy). Poza tym taki rodzaj komunikacji wymaga żeby obie osoby były dostępne w tym samym momencie bo inaczej zadanie przekazania informacji się nie powiedzie i po odczekaniu kilku sygnałów odłożymy telefon (nastąpi timeout).

Komunikacja asynchroniczna

W przeciwieństwie do telefonu email jest przykładem komunikacji asynchronicznej. Nie musimy czekać aż druga osoba włączy komputer żeby móc jej wysłać maila. Po prostu to robimy i wracamy do zajmowania się innymi sprawami. A jak przyjdzie odpowiedź to do niej wrócimy. Jeśli tylko wiadomość się nie przedawni to nie interesuje nas kiedy dokładnie druga strona odebrała nasz komunikat. Co więcej możemy takich wiadomości wysłać kilka pod rząd. A to kiedy i w jakiej kolejności odbiorca je „przetworzy” to już nie nasza sprawa.

W ten sposób możemy optymalnie wykorzystać czas robiąc kolejne zadania dopóki nie nadejdzie wiadomość zwrotna albo inny email od kogoś. Bo wyobraźmy sobie jakby wyglądała komunikacja mailowa gdyby była synchroniczna jak telefon: czekamy aż zobaczymy, że druga osoba jest dostępna (np. zmienił się jej status w komunikatorze) -> wysyłamy wiadomość -> patrzymy się w okno klienta pocztowego w oczekiwaniu na odpowiedź, nie możemy w tym czasie wysłać kolejnej wiadomości, np. do kogoś innego -> druga osoba otwiera i czyta maila -> odpisuje na niego z pytaniem -> dostajemy odpowiedź i zabieramy się za wysłanie szczegółów -> teraz ponownie czekamy na odpowiedź…

I tak aż do zakończenia rozmowy. Dopiero po takiej wymianie maili moglibyśmy wstać i pójść coś zjeść albo napisać do kogoś innego w sprawie innego zadania. Brzmi absurdalnie. A dokładnie tak działają nasze programy kiedy piszemy w nich synchroniczny kod, który komunikuje się ze „światem zewnętrznym” takim jak baza danych czy inny serwis!

Nie blokuj

Podsumowując, programowanie asynchroniczne czy korzystanie z asynchroniczności w projekcie to budowanie kodu w takich sposób, żeby komunikacja ze światem zewnętrznym nie powodowała, że wszystkie kolejne zadania muszą czekać aż ta komunikacja łaskawie się zakończy. Nawet jeżeli w żaden sposób nie potrzebują wyników jakie ona przyniesie. Powinniśmy czekać na zakończenie zadania dopiero w momencie kiedy kolejne zadanie wymaga wyników poprzedniego.

Zobaczmy więc teraz w jaki sposób to zrealizować w naszym kodzie.

Async/await w ASP.NET Core

Słowa kluczowe async i await weszły to języka C# wraz z wersją 5.0, a więc w roku 2012.

I od tego momentu przyczyniają się do ułatwiania programistom życia w kwestii pisania kodu asynchronicznego.

Mógłbym tutaj założyć, że jeżeli znasz C# to async i await są Ci dobrze znane. Jednak być może uczyłeś się C# przed tym 2012 rokiem? Albo jeszcze nie doszedłeś z wiedzą do tego momentu. Dlatego pokrótce przybliżę Ci temat.

Czym jest async i await?

Na samym początku można powiedzieć, że async i await to w rzeczywistości „lukier składniowy” na maszynę stanów, która sprawdza czy można już dalej wykonywać kod czy nie.

Jeżeli używamy await w kodzie naszej funkcji to każde jego użycie dzieli tą funkcję na fragment do await i fragment po await. I zadanie, na które czekamy wrzucane jest do osobnego wątku. I maszyna stanów sprawdza czy stan zadania już się zmienił. W momencie jak zostało zakończone to idzie dalej i wykonuje kolejny krok. Kolejny stan już operujący na danych z asynchronicznego zadania.

Chcąc korzystać z async/await tak naprawdę korzystamy przede wszystkim z klasy Task. Bo to właśnie obiekt tej klasy jest tworzony i obok niego stawiamy słowo await. Tak więc tworzony jest Task, który ma wykonać jakieś zadanie, a async i await po prostu pozwalają korzystać z niego tak jakbyśmy pisali kod synchroniczny. Bo nadal przypisujemy rezultat funkcji przekazanej do Taska tak jakbyśmy ją bezpośrednio wykonywali – po prostu dodajemy słowo kluczowe await.

Jeżeli metoda korzysta z polecenia await to powinna być oznaczona słowem kluczowym async. W ten sposób dajemy znać kompilatorowi, że w tej funkcji będzie wykonywany asynchroniczny kod.

private async void Calc(int x)
{
    var result = await Task.Run(() => x * 10);
    Console.WriteLine($"result = {result}");
}

Jeżeli zwracamy wynik z asynchronicznej metody to typem zwracanym będzie tak naprawdę Task<T> gdzie T jest naszym docelowym typem:

private async Task<int> Calc(int x)
{
    var result = await Task.Run(() => x * 10);
    return result;
}

public async void Method()
{
    var result = await Calc(10);
    Console.WriteLine($"result = {result}");
}

Asynchroniczne API

Jednym z parametrów, na które został postawiony nacisk w przypadku tworzenia ASP.NET Core była wydajność i liczba obsługiwanych jednocześnie użytkowników. Żeby to osiągnąć to nie można było zignorować tematu asynchroniczności. W końcu jak możemy optymalnie wykorzystywać serwer jeżeli jakieś zapytanie zablokowało wątek bo czeka aż baza zwróci potrzebną wartość?

Ważne jest tutaj, że asynchroniczność dla jednego requestu nie zrobi różnicy, czas wykonania się nie zmieni. Zrobi ona różnicę dopiero kiedy zaczniemy porównywać ilość obsłużonych zapytań na sekundę. Bo dopiero wtedy asynchroniczność zacznie być widoczna ponieważ zapytania czekające na jakieś zadanie nie będą blokowały przyjmowania kolejnych zapytań, które być może nie będą oczekiwać na odpowiedź bazy albo zakończą się dużo szybciej.

Dlatego ASP.NET Core od początku i maksymalnie wspiera asynchroniczne operacje. Praktycznie wszystkie metody, które mogłyby blokować wątek bo np. odwołują się do innych zasobów albo mogłyby być czasochłonne posiadają swoje asynchroniczne odpowiedniki.

Zróbmy to!

Zrobienie asynchronicznego zapytania do bazy danych z wykorzystaniem async/await wymaga żeby również metoda, która je wykonuje była asynchroniczna. W ten sposób dochodzimy do najwyższego punktu naszego procesu czyli do endpointu. Chcąc zrobić asynchroniczne odpytanie bazy danych tak naprawdę musimy zacząć od przygotowania asynchronicznego endpointu.

Przyjrzyjmy się poniższemu kodowi. Pominąłem w nim szczegóły takie jak wstrzykiwanie obiektów tak aby nie zaciemniać najważniejszych elementów:

// UserController.cs

[ApiController]
[Route("api/user")
class UserController : ControllerBase
{
    private readonly UserService userService;

    // ....

    [HttpGet("{id}")]
    public async Task<UserDto> Get(long id)
    {
        return await userService.GetUserAsync(id);
    }
}

// UserService.cs

class UserService
{
    private readonly AppDbContext _context;

    // ...

    public async Task<UserDto> GetUserAsync(long id)
    {
        var user = await _context.SingleAsync(x => x.Id == id);
        return new UserDto
        {
            Id = user.Id,
            Name = $"{user.FirstName} {user.LastName}"
        }
    }
}

Jak widzisz wszystkie metody w procesie, począwszy od akcji kontrolera, zostały oznaczone jako async. Dzięki temu wewnątrz nich możemy wykonać polecenie await. I tak dochodzimy aż do funkcji GetUserAsync(), która odwołuje się do bazy danych.

Jak to się zachowa?

Wykonanie tego całego procesu będzie wyglądać w ten sposób, że kiedy przyjdzie request to zostaje on odebrany, akcja kontrolera przekaże ID do serwisu, następnie metoda w serwisie zapyta bazę danych o użytkownika. W tym momencie następuje STOP. Operacja oczekiwania na dane ląduje jakby w wątku pobocznym, który korzysta z puli dostępnych wątków. A wątek, w którym wykonywane było zapytanie jest oddawany do użytku i teraz aplikacja może na nim przyjąć kolejne zapytanie, np. kolejne pytanie o użytkownika. Wtedy znowu dochodzimy do odpytywania bazy i ponownie operacja oczekiwania na dane ląduje na bocznym torze. I być może przez ten czas poprzednie dane już do nas dotarły z bazy więc możemy wrócić do ich obsługi. Do tego celu możemy wykorzystać poprzednio używany wątek bo drugi request go oddał czekając na połączenie z bazą.

Podsumowując

W ten sposób korzystając z async/await usprawniamy obsługę zapytań. Nie ma pustych przebiegów. W momencie oczekiwania na coś już kolejne zapytania mogą być przyjmowane.

Możesz to sobie wyobrazić jak np. kolejkę w sklepie gdzie pracują dwie osoby. Każda z nich to osobny wątek. I teraz kiedy piszesz synchroniczny kod to tak jakby osoba przy kasie po dowiedzeniu się czego klient chce szukała czy towar jest dostępny. Następnie po niego szła i na koniec nabijała rachunek. Ale jeżeli piszesz kod asynchroniczny to w naszym sklepie wyglądało by to w ten sposób, że pierwsza osoba sprawdziłaby czy towar jest dostępny i wysłała po niego drugą osobę. A kiedy druga osoba szukałaby produktu w magazynie to pierwsza mogłaby zapytać kolejną osobę w kolejce czego potrzebuje. I zanim drugi pracownik przyniósłby pierwszy towar to druga osoba by już miała sprawdzone czy rzecz, której potrzebuje jest na stanie. I być może by poszła kiedy by go nie było, albo od razu poprosiła o jego przyniesienie kiedy tylko produkt dla pierwszego klienta zostałby znaleziony w magazynie.

W taki oto sposób działają operacje asynchroniczne w aplikacji webowej pianej z wykorzystaniem async i await. Zachęcam Cię do zapoznania się z nimi i stosowania ;)

2 komentarze

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *