Programowanie asynchroniczne

Jeśli masz jakiekolwiek potrzeby związane z operacjami we/wy (np. żądanie danych z sieci, uzyskiwanie dostępu do bazy danych lub odczytywanie i zapisywanie w systemie plików), warto użyć programowania asynchronicznego. Możesz również mieć kod powiązany z procesorem CPU, taki jak wykonywanie kosztownych obliczeń, co jest również dobrym scenariuszem do pisania kodu asynchronicznego.

Język C# ma asynchroniczny model programowania na poziomie języka, który umożliwia łatwe pisanie kodu asynchronicznego bez konieczności przełączania wywołań zwrotnych ani zgodności z biblioteką, która obsługuje asynchronię. Jest on zgodny z wzorcem asynchronicznym opartym na zadaniach (TAP, Task-based Asynchronous Pattern).

Omówienie modelu asynchronicznego

Rdzeniem programowania asynchronicznego są obiekty Task i Task<T> , które modeluje operacje asynchroniczne. Są one obsługiwane przez słowa async await kluczowe i . Model jest dość prosty w większości przypadków:

  • W przypadku kodu powiązanego z operacjami we/wy oczekujesz na operację, która zwraca metodę lub Task Task<T> wewnątrz metody async .
  • W przypadku kodu powiązanego z procesorem CPU oczekujesz na operację, która jest uruchomiona w wątku w tle za pomocą Task.Run metody .

Słowo await kluczowe to miejsce, w którym dzieje się magia. Daje to kontrolę elementowi wywołującemu metodę , która wykonała , i ostatecznie umożliwia interfejsowi użytkownika odpowiadanie lub elastyczne await korzystanie z usługi. Chociaż istnieją sposoby podejścia do kodu asynchronicznego innego niż i , ten artykuł koncentruje się na async await konstrukcjach na poziomie języka.

Przykład powiązany z we/wy: pobieranie danych z usługi internetowej

Po naciśnięciu przycisku może być konieczne pobranie danych z usługi internetowej, ale nie chcesz blokować wątku interfejsu użytkownika. Można to zrobić w ten sposób:

private readonly HttpClient _httpClient = new HttpClient();

downloadButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI as the request
    // from the web service is happening.
    //
    // The UI thread is now free to perform other work.
    var stringData = await _httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

Kod wyraża intencję (pobieranie danych asynchronicznie) bez tagów podczas interakcji z Task obiektami.

Przykład związany z procesorem CPU: wykonywanie obliczeń dla gry

Załóżmy, że piszesz mobilną grę, w której naciśnięcie przycisku może wyrządzić szkody w wielu miejscach na ekranie. Obliczanie szkód może być kosztowne, a wykonanie go w wątku interfejsu użytkownika sprawi, że gra zostanie wstrzymana w trakcie wykonywania obliczeń.

Najlepszym sposobem obsługi tego jest uruchomienie wątku w tle, który działa przy użyciu funkcji i oczekuje Task.Run na wynik przy użyciu funkcji await . Dzięki temu interfejs użytkownika może działać bezproblemowo podczas pracy.

private DamageResult CalculateDamageDone()
{
    // Code omitted:
    //
    // Does an expensive calculation and returns
    // the result of that calculation.
}

calculateButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI while CalculateDamageDone()
    // performs its work. The UI thread is free to perform other work.
    var damageResult = await Task.Run(() => CalculateDamageDone());
    DisplayDamage(damageResult);
};

Ten kod wyraźnie wyraża intencję zdarzenia kliknięcia przycisku, nie wymaga ręcznego zarządzania wątkiem w tle i robi to w sposób nieblokowania.

Co się dzieje w ramach okładki

Istnieje wiele ruchomych elementów, których dotyczą operacje asynchroniczne. Jeśli zastanawiasz się, co się dzieje poniżej obszarów i , zobacz artykuł Task Task<T> Async in-depth (Asynchroniczne szczegółowe informacje), aby uzyskać więcej informacji.

Po stronie języka C# kompilator przekształca kod w maszynę stanu, która śledzi rzeczy takie jak uzyskanie wykonania po osiągnięciu i wznowienie wykonywania po zakończeniu zadania w await tle.

Teoretycznie jest to implementacja modelu obietnicy asynchronicznej.

Kluczowe elementy do zrozumienia

  • Kod asynchroniczny może być używany zarówno w przypadku kodu powiązanego z we/wy, jak i kodu powiązanego z procesorem CPU, ale różni się w poszczególnych scenariuszach.
  • Kod asynchroniczny używa i , które są konstrukcjami używanymi do Task<T> Task modelowania pracy wykonywanej w tle.
  • Słowo async kluczowe zamienia metodę w metodę asynchroniczną, która umożliwia użycie słowa await kluczowego w jego treści.
  • Zastosowanie await słowa kluczowego wstrzymuje metodę wywołującą i daje kontrolę do jej wywołującego do czasu ukończenia oczekiwanego zadania.
  • await Można go używać tylko wewnątrz metody asynchronicznej.

Rozpoznawanie pracy związanej z procesorem CPU i we/wy

Pierwsze dwa przykłady tego przewodnika pokazują, jak można używać funkcji i do pracy związanej z we/wy i async await procesorem CPU. Kluczowe jest, aby określić, kiedy zadanie jest związane z we/wy lub procesorem CPU, ponieważ może mieć duży wpływ na wydajność kodu i potencjalnie może prowadzić do błędnego użycia niektórych konstrukcji.

Poniżej znajdują się dwa pytania, które należy zadać przed napisem kodu:

  1. Czy kod będzie "czekał" na coś, na przykład dane z bazy danych?

    Jeśli odpowiedź brzmi "tak", Oznacza to, że Twoja praca jest powiązana z we/wy.

  2. Czy kod będzie wykonywać kosztowne obliczenia?

    Jeśli odpowiesz "tak", Twoja praca jest powiązana z procesorem CPU.

Jeśli praca jest powiązana z we/wy, użyj i async await bez Task.Run . Nie należy używać biblioteki zadań równoległych. Przyczynę tego działania opisano w te tematze Async in Depth (Asynchroniczne w głębi).

Jeśli praca jest powiązana z procesorem CPU i zależy Ci na czasie odpowiedzi, użyj i , ale odtąd wyłącz pracę async w innym wątku za pomocą await funkcji Task.Run . Jeśli praca jest odpowiednia dla współbieżności i równoległości, należy również rozważyć użycie biblioteki zadań równoległych.

Ponadto zawsze należy mierzyć wykonywanie kodu. Na przykład może się okazać, że praca związana z procesorem CPU nie jest wystarczająco kosztowna w porównaniu z obciążeniem przełączników kontekstu podczas wielowątkowania. Każdy wybór ma swoje kompromisy i należy wybrać odpowiedni kompromis dla swojej sytuacji.

Więcej przykładów

W poniższych przykładach pokazano różne sposoby pisania kodu asynchronicznego w języku C#. Obejmują one kilka różnych scenariuszy, które możesz zetknąć.

Wyodrębnianie danych z sieci

Ten fragment kodu pobiera kod HTML ze strony głównej pod i zlicza liczbę wystąpień ciągu https://dotnetfoundation.org ".NET" w kodzie HTML. Używa ona ASP.NET do zdefiniowania metody kontrolera interfejsu API sieci Web, która wykonuje to zadanie i zwraca liczbę.

Uwaga

Jeśli planujesz analizowanie kodu HTML w kodzie produkcyjnym, nie używaj wyrażeń regularnych. Zamiast tego użyj biblioteki do analizowania.

private readonly HttpClient _httpClient = new HttpClient();

[HttpGet, Route("DotNetCount")]
public async Task<int> GetDotNetCount()
{
    // Suspends GetDotNetCount() to allow the caller (the web server)
    // to accept another request, rather than blocking on this one.
    var html = await _httpClient.GetStringAsync("https://dotnetfoundation.org");

    return Regex.Matches(html, @"\.NET").Count;
}

Oto ten sam scenariusz, który został napisany dla aplikacji universal Windows App, która wykonuje to samo zadanie po naciśnięciu przycisku:

private readonly HttpClient _httpClient = new HttpClient();

private async void OnSeeTheDotNetsButtonClick(object sender, RoutedEventArgs e)
{
    // Capture the task handle here so we can await the background task later.
    var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://dotnetfoundation.org");

    // Any other work on the UI thread can be done here, such as enabling a Progress Bar.
    // This is important to do here, before the "await" call, so that the user
    // sees the progress bar before execution of this method is yielded.
    NetworkProgressBar.IsEnabled = true;
    NetworkProgressBar.Visibility = Visibility.Visible;

    // The await operator suspends OnSeeTheDotNetsButtonClick(), returning control to its caller.
    // This is what allows the app to be responsive and not block the UI thread.
    var html = await getDotNetFoundationHtmlTask;
    int count = Regex.Matches(html, @"\.NET").Count;

    DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";

    NetworkProgressBar.IsEnabled = false;
    NetworkProgressBar.Visibility = Visibility.Collapsed;
}

Oczekiwanie na ukończenie wielu zadań

Może się okazać, że musisz pobrać wiele elementów danych jednocześnie. Interfejs API zawiera dwie metody i , które umożliwiają pisanie kodu asynchronicznego, który wykonuje nieblokowanie Task oczekiwania na wiele zadań w Task.WhenAll Task.WhenAny tle.

W tym przykładzie pokazano, jak można User pobrać dane dla zestawu userId s.

public async Task<User> GetUserAsync(int userId)
{
    // Code omitted:
    //
    // Given a user Id {userId}, retrieves a User object corresponding
    // to the entry in the database with {userId} as its Id.
}

public static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = new List<Task<User>>();
    foreach (int userId in userIds)
    {
        getUserTasks.Add(GetUserAsync(userId));
    }

    return await Task.WhenAll(getUserTasks);
}

Oto inny sposób, aby napisać to zwięźlej przy użyciu LINQ:

public async Task<User> GetUserAsync(int userId)
{
    // Code omitted:
    //
    // Given a user Id {userId}, retrieves a User object corresponding
    // to the entry in the database with {userId} as its Id.
}

public static async Task<User[]> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = userIds.Select(id => GetUserAsync(id));
    return await Task.WhenAll(getUserTasks);
}

Chociaż jest to mniej kodu, należy zachować ostrożność podczas łączenia linq z kodem asynchronicznym. Ze względu na to, że LINQ używa wykonywania odroczonego (z opóźnieniem), wywołania asynchroniczne nie będą odbywać się natychmiast, tak jak w pętli, chyba że wymusz iteruj wygenerowaną sekwencję za pomocą wywołania foreach .ToList() do lub .ToArray() .

Ważne informacje i porady

W przypadku programowania asynchronicznego należy pamiętać o pewnych szczegółach, które mogą zapobiec nieoczekiwanemu zachowaniu.

  • asyncmetody muszą mieć await słowo kluczowe w ich treści lub nigdy nie daje!

    Należy o tym pamiętać. Jeśli element nie jest używany w treści metody, kompilator języka C# generuje ostrzeżenie, ale kod jest kompilowany i uruchamiany tak, jakby był await async normalną metodą. Jest to niezwykle nieefektywne, ponieważ maszyna stanu wygenerowana przez kompilator języka C# dla metody asynchronicznej nie wykonuje żadnych czynności.

  • Dodaj "Async" jako sufiks każdej zapisywanych nazw metod asynchronicznych.

    Jest to konwencja używana w programie .NET, aby łatwiej rozróżniać metody synchroniczne i asynchroniczne. Niektóre metody, które nie są jawnie wywoływane przez kod (takie jak procedury obsługi zdarzeń lub metody kontrolera sieci Web), niekoniecznie mają zastosowanie. Ponieważ nie są jawnie wywoływane przez kod, jawne nazewnictwo nie jest tak ważne.

  • async voidPowinien być używany tylko dla programów obsługi zdarzeń.

    async void jest jedynym sposobem na to, aby asynchroniczne procedury obsługi zdarzeń działały, ponieważ zdarzenia nie mają zwracanych typów (w związku z tym nie mogą używać metod Task i Task<T> ). Każde inne użycie nie jest zgodne z modelem TAP i może być trudne async void do użycia, takie jak:

    • Wyjątków async void zgłaszanych w metodzie nie można przechwycić poza metodą .
    • async void Metody są trudne do przetestowania.
    • async void Metody mogą powodować złe skutki uboczne, jeśli wywołujący nie oczekuje, że będą asynchroniczne.
  • Dokładne rozważne stosowanie asynchronicznych wyrażeń lambda w wyrażeniach LINQ

    Wyrażenia lambda w LINQ używają wykonania odroczonego, co oznacza, że kod może kończyć się wykonywaniem w czasie, gdy nie jest to spodziewane. Wprowadzenie do tego zadania blokującego może łatwo spowodować zakleszczenie, jeśli nie zostało poprawnie zapisane. Ponadto zagnieżdżanie kodu asynchronicznego, takiego jak ten, może również utrudnić uzasadnienie wykonania kodu. Asynchroniczne i LINQ są zaawansowane, ale powinny być używane razem tak starannie i wyraźnie, jak to możliwe.

  • Pisanie kodu, który oczekuje na zadania w sposób nieblokujący

    Zablokowanie bieżącego wątku jako środka do oczekiwania na zakończenie a może spowodować zakleszczenia i zablokowane wątki kontekstu i może wymagać bardziej Task złożonej obsługi błędów. W poniższej tabeli przedstawiono wskazówki dotyczące sposobu radzenia sobie z oczekiwaniem na zadania w sposób nieblokący:

    Użyj polecenia... Zamiast tego... Gdy chcesz to zrobić...
    await Task.Wait lub Task.Result Pobieranie wyniku zadania w tle
    await Task.WhenAny Task.WaitAny Oczekiwanie na ukończenie dowolnego zadania
    await Task.WhenAll Task.WaitAll Oczekiwanie na ukończenie wszystkich zadań
    await Task.Delay Thread.Sleep Oczekiwanie przez określony czas
  • Rozważ użycie ValueTask tam, gdzie to możliwe

    Zwracanie obiektu Task z metod asynchronicznych może wprowadzać wąskie gardła wydajności w niektórych ścieżkach. Task jest typem referencyjnym, więc użycie go oznacza przydzielenie obiektu. W przypadkach, gdy metoda zadeklarowana za pomocą modyfikatora zwraca wynik z pamięci podręcznej lub kończy się synchronicznie, dodatkowe alokacje mogą stać się znaczącym kosztem czasu w krytycznych dla wydajności sekcjach async kodu. Może to być kosztowne, jeśli te alokacje występują w pętli ścisłej. Aby uzyskać więcej informacji, zobacz uogólnione asynchroniczne typy zwracane.

  • Rozważ użycie ConfigureAwait(false)

    Często zadawane pytanie brzmi: "Kiedy należy użyć Task.ConfigureAwait(Boolean) metody?". Metoda umożliwia wystąpieniu Task skonfigurowanie jego awaiter. Jest to ważna kwestią i nieprawidłowe ustawienie go może mieć potencjalnie wpływ na wydajność, a nawet zakleszczenia. Aby uzyskać więcej informacji na ConfigureAwait temat usługi , zobacz ConfigureAwait FAQ (Konfigurowanie aplikacji— często zadawane pytania).

  • Pisanie mniej stanowego kodu

    Nie zależą od stanu obiektów globalnych ani od wykonania niektórych metod. Zamiast tego należy polegać tylko na zwracanych wartościach metod. Dlaczego?

    • Kod będzie łatwiejszy do uzasadnienia.
    • Kod będzie łatwiejszy do przetestowania.
    • Połączenie kodu asynchronicznego i synchronicznego jest znacznie prostsze.
    • Sytuacji wyścigu można zwykle całkowicie uniknąć.
    • W zależności od zwracanych wartości koordynowanie kodu asynchronicznego jest proste.
    • (Dodatkowa) działa bardzo dobrze w przypadku wstrzykiwania zależności.

Zalecanym celem jest osiągnięcie pełnej lub niemal pełnej przezroczystości referencyjnej w kodzie. Spowoduje to przewidywalną, testowalną i podtrzymywalną bazę kodu.

Inne zasoby