Scenariusze programowania asynchronicznego

Jeśli masz jakiekolwiek potrzeby związane z operacjami we/wy (takie jak żądanie danych z sieci, uzyskiwanie dostępu do bazy danych lub odczytywanie i zapisywanie w systemie plików), należy 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 żonglowania wywołań zwrotnych lub zgodności z biblioteką, która obsługuje asynchronię. Następuje to, co jest znane jako wzorzec asynchroniczny oparty na zadaniach (TAP).

Omówienie modelu asynchronicznego

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

  • W przypadku kodu powiązanego z we/wy oczekujesz operacji zwracającej metodę Task lub Task<T> wewnątrz niej async .
  • W przypadku kodu powiązanego z procesorem CPU oczekujesz na operację uruchomioną w wątku w tle za pomocą Task.Run metody .

Słowo await kluczowe to miejsce, w którym dzieje się magia. Daje kontrolę nad obiektem wywołującym metodę, która wykonała awaitmetodę , i ostatecznie pozwala interfejsowi użytkownika reagować lub usługa być elastyczna. Chociaż istnieją sposoby podejścia do kodu asynchronicznego innego niż async i await, ten artykuł koncentruje się na konstrukcjach na poziomie języka.

Uwaga

W niektórych z poniższych przykładów System.Net.Http.HttpClient klasa służy do pobierania danych z usługi internetowej. Obiekt s_httpClient używany w tych przykładach jest polem statycznym Program klasy (sprawdź kompletny przykład):

private static readonly HttpClient s_httpClient = new();

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

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

s_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 s_httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

Kod wyraża intencję (pobieranie danych asynchronicznie) bez zakłócania interakcji z obiektami Task .

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

Załóżmy, że piszesz grę mobilną, w której naciśnięcie przycisku może wyrządzić szkody wielu wrogów na ekranie. Wykonanie obliczeń szkody może być kosztowne i wykonanie go w wątku interfejsu użytkownika sprawi, że gra stanie się wstrzymana, gdy obliczenia są wykonywane!

Najlepszym sposobem obsługi tej metody jest uruchomienie wątku w tle, który działa przy użyciu metody i oczekuje na jego wynik przy użyciu Task.Runpolecenia await. Dzięki temu interfejs użytkownika może czuć się gładko podczas wykonywania pracy.

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

s_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 nieblokcyjny.

Co się dzieje pod osłonami

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

Teoretycznie jest to implementacja modelu obietnicy asynchronii.

Kluczowe elementy do zrozumienia

  • Kod asynchroniczny może być używany zarówno dla kodu powiązanego we/wy, jak i powiązanego z procesorem CPU, ale inaczej dla każdego scenariusza.
  • Kod asynchroniczny używa metod Task<T> i Task, które są konstrukcjami używanymi do modelowania pracy wykonywanej w tle.
  • Słowo async kluczowe zamienia metodę w metodę asynchroniową, która umożliwia użycie słowa kluczowego await w jego treści.
  • Po zastosowaniu await słowa kluczowego zawiesza metodę wywołującą i zwraca kontrolę do obiektu wywołującego do momentu ukończenia oczekiwanego zadania.
  • await można używać tylko wewnątrz metody asynchronicznej.

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

W pierwszych dwóch przykładach tego przewodnika pokazano, jak można używać operacji async we await /wy i związanych z procesorem CPU oraz do pracy powiązanej z procesorem CPU. Kluczem jest określenie, kiedy należy wykonać zadanie, jest związane z we/wy lub związane z procesorem CPU, ponieważ może to znacznie wpłynąć na wydajność kodu i może potencjalnie prowadzić do błędnego użycia niektórych konstrukcji.

Poniżej przedstawiono dwa pytania, które należy zadać przed napisaniem kodu:

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

    Jeśli odpowiedź brzmi "tak", twoja praca jest powiązana we/wy.

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

    Jeśli otrzymasz odpowiedź "tak", twoja praca jest powiązana z procesorem CPU.

Jeśli masz pracę związaną z we/wy, użyj polecenia async i awaitbezTask.Run niego. Nie należy używać biblioteki równoległej zadań.

Jeśli praca, którą masz, jest powiązana z procesorem CPU i zależy Ci na reakcji, użyciu async i await, ale zduplikuj pracę w innym wątku za pomocąTask.Run polecenia. Jeśli praca jest odpowiednia dla współbieżności i równoległości, rozważ również użycie biblioteki równoległej zadań.

Ponadto zawsze należy zmierzyć wykonywanie kodu. Na przykład możesz znaleźć się w sytuacji, w której praca związana z procesorem CPU nie jest wystarczająco kosztowna w porównaniu z obciążeniem przełączników kontekstowych podczas wielowątku. Każdy wybór ma swój kompromis i należy wybrać prawidłowy kompromis dla twojej 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, na które możesz się natknąć.

Wyodrębnianie danych z sieci

Ten fragment kodu pobiera kod HTML z danego adresu URL i zlicza liczbę wystąpień ciągu .NET w kodzie HTML. Używa ASP.NET do definiowania metody kontrolera internetowego interfejsu API, 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 analizowania.

[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
    // Suspends GetDotNetCount() to allow the caller (the web server)
    // to accept another request, rather than blocking on this one.
    var html = await s_httpClient.GetStringAsync(URL);
    return Regex.Matches(html, @"\.NET").Count;
}

Oto ten sam scenariusz napisany dla aplikacji uniwersalnej systemu Windows, który 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;
}

Poczekaj na ukończenie wielu zadań

Możesz znaleźć się w sytuacji, w której trzeba jednocześnie pobrać wiele fragmentów danych. Interfejs Task API zawiera dwie metody Task.WhenAll i Task.WhenAny, które umożliwiają pisanie kodu asynchronicznego, który wykonuje nieblokujące oczekiwanie na wiele zadań w tle.

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

private static 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.

    return await Task.FromResult(new User() { id = userId });
}

private 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 bardziej zwięźle, używając LINQ:

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

Mimo że jest to mniej kodu, należy zachować ostrożność podczas mieszania LINQ z kodem asynchronicznym. Ponieważ LINQ używa odroczonego (opóźnionego) wykonywania, wywołania asynchroniczne nie będą wykonywane natychmiast w foreach pętli, chyba że wymusisz iterowanie wygenerowanej sekwencji za pomocą wywołania metody .ToList() lub .ToArray(). W powyższym przykładzie użyto Enumerable.ToArray metody do wykonywania zapytania z niecierpliwością i przechowywania wyników w tablicy. Wymusza to uruchomienie i uruchomienie zadania przez kod id => GetUserAsync(id) .

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ćawaitsłowo kluczowe w ich treści lub nigdy nie dadzą!

    Ważne jest, aby pamiętać o tym. Jeśli await nie jest używana w treści async metody, kompilator języka C# generuje ostrzeżenie, ale kod kompiluje i uruchamia tak, jakby była to normalna metoda. Jest to niezwykle nieefektywne, ponieważ maszyna stanu wygenerowana przez kompilator języka C# dla metody asynchronicznej nie wykonuje żadnych działań.

  • Dodaj "Async" jako sufiks każdej zapisanej nazwy metody asynchronicznej.

    Jest to konwencja używana na platformie .NET do łatwiejszego rozróżniania metod synchronicznych i asynchronicznych. Niektóre metody, które nie są jawnie wywoływane przez kod (takie jak procedury obsługi zdarzeń lub metody kontrolera sieci Web), nie muszą być stosowane. Ponieważ nie są one jawnie wywoływane przez kod, jawne nazewnictwo nie jest tak ważne.

  • async voidpowinny być używane tylko w przypadku programów obsługi zdarzeń.

    async void to jedyny sposób zezwalania na działanie asynchronicznych programów obsługi zdarzeń, ponieważ zdarzenia nie mają typów zwracanych (w związku z Task czym nie można używać elementów i Task<T>). Każde inne użycie elementu nie jest zgodne z async void modelem TAP i może być trudne do użycia, takie jak:

    • Wyjątki zgłoszone w metodzie async void nie mogą być przechwytywane poza tą metodą.
    • async void metody są trudne do przetestowania.
    • async void metody mogą powodować złe skutki uboczne, jeśli obiekt wywołujący nie oczekuje ich asynchronicznego.
  • Starannie przeczytaj podczas używania asynchronicznych wyrażeń LINQ

    Wyrażenia lambda w linQ używają odroczonego wykonywania, co oznacza, że kod może być wykonywany w czasie, gdy nie oczekujesz, że. Wprowadzenie do tego zadania blokującego może łatwo spowodować zakleszczenie, jeśli nie zostało napisane poprawnie. Ponadto zagnieżdżanie kodu asynchronicznego, takiego jak ten, może utrudnić rozumowanie wykonywania kodu. Async i LINQ są zaawansowane, ale powinny być używane razem tak ostrożnie 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 oczekiwania na Task ukończenie może spowodować zakleszczenia i zablokowane wątki kontekstowe i może wymagać bardziej złożonej obsługi błędów. Poniższa tabela zawiera wskazówki dotyczące sposobu radzenia sobie z oczekiwaniem na zadania w sposób nieblokcyjny:

    Użyj polecenia... Zamiast tego... Jeśli 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 na pewien czas
  • Rozważ użycieValueTasktam, gdzie to możliwe

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

  • Rozważ użycieConfigureAwait(false)

    Często zadawane pytanie brzmi: "kiedy należy użyć Task.ConfigureAwait(Boolean) metody?". Metoda umożliwia wystąpieniu Task skonfigurowanie jego funkcji awaiter. Jest to ważna kwestia i niepoprawne ustawienie może mieć wpływ na wydajność, a nawet zakleszczenia. Aby uzyskać więcej informacji na ConfigureAwaittemat programu , zobacz ConfigureAwait FAQ (Często zadawane pytania dotyczące konfigurowania elementu ConfigureAwait).

  • Pisanie mniej kodów stanowych

    Nie zależy od stanu obiektów globalnych ani wykonywania określonych metod. Zamiast tego zależy tylko od zwracanych wartości metod. Dlaczego?

    • Kod będzie łatwiejszy do rozumowania.
    • Kod będzie łatwiejszy do przetestowania.
    • Mieszanie kodu asynchronicznego i synchronicznego jest znacznie prostsze.
    • Warunki wyścigowe można zwykle unikać całkowicie.
    • W zależności od wartości zwracanych koordynacja kodu asynchronicznego jest prosta.
    • (Bonus) działa naprawdę dobrze z wstrzykiwania zależności.

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

Kompletny przykład

Poniższy kod jest kompletnym tekstem pliku Program.cs dla przykładu.

using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.AspNetCore.Mvc;

class Button
{
    public Func<object, object, Task>? Clicked
    {
        get;
        internal set;
    }
}

class DamageResult
{
    public int Damage
    {
        get { return 0; }
    }
}

class User
{
    public bool isEnabled
    {
        get;
        set;
    }

    public int id
    {
        get;
        set;
    }
}

public class Program
{
    private static readonly Button s_downloadButton = new();
    private static readonly Button s_calculateButton = new();

    private static readonly HttpClient s_httpClient = new();

    private static readonly IEnumerable<string> s_urlList = new string[]
    {
            "https://learn.microsoft.com",
            "https://learn.microsoft.com/aspnet/core",
            "https://learn.microsoft.com/azure",
            "https://learn.microsoft.com/azure/devops",
            "https://learn.microsoft.com/dotnet",
            "https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio",
            "https://learn.microsoft.com/education",
            "https://learn.microsoft.com/shows/net-core-101/what-is-net",
            "https://learn.microsoft.com/enterprise-mobility-security",
            "https://learn.microsoft.com/gaming",
            "https://learn.microsoft.com/graph",
            "https://learn.microsoft.com/microsoft-365",
            "https://learn.microsoft.com/office",
            "https://learn.microsoft.com/powershell",
            "https://learn.microsoft.com/sql",
            "https://learn.microsoft.com/surface",
            "https://dotnetfoundation.org",
            "https://learn.microsoft.com/visualstudio",
            "https://learn.microsoft.com/windows",
            "https://learn.microsoft.com/xamarin"
    };

    private static void Calculate()
    {
        // <PerformGameCalculation>
        static DamageResult CalculateDamageDone()
        {
            return new DamageResult()
            {
                // Code omitted:
                //
                // Does an expensive calculation and returns
                // the result of that calculation.
            };
        }

        s_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);
        };
        // </PerformGameCalculation>
    }

    private static void DisplayDamage(DamageResult damage)
    {
        Console.WriteLine(damage.Damage);
    }

    private static void Download(string URL)
    {
        // <UnblockingDownload>
        s_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 s_httpClient.GetStringAsync(URL);
            DoSomethingWithData(stringData);
        };
        // </UnblockingDownload>
    }

    private static void DoSomethingWithData(object stringData)
    {
        Console.WriteLine("Displaying data: ", stringData);
    }

    // <GetUsersForDataset>
    private static 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.

        return await Task.FromResult(new User() { id = userId });
    }

    private 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);
    }
    // </GetUsersForDataset>

    // <GetUsersForDatasetByLINQ>
    private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
    {
        var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
        return await Task.WhenAll(getUserTasks);
    }
    // </GetUsersForDatasetByLINQ>

    // <ExtractDataFromNetwork>
    [HttpGet, Route("DotNetCount")]
    static public async Task<int> GetDotNetCount(string URL)
    {
        // Suspends GetDotNetCount() to allow the caller (the web server)
        // to accept another request, rather than blocking on this one.
        var html = await s_httpClient.GetStringAsync(URL);
        return Regex.Matches(html, @"\.NET").Count;
    }
    // </ExtractDataFromNetwork>

    static async Task Main()
    {
        Console.WriteLine("Application started.");

        Console.WriteLine("Counting '.NET' phrase in websites...");
        int total = 0;
        foreach (string url in s_urlList)
        {
            var result = await GetDotNetCount(url);
            Console.WriteLine($"{url}: {result}");
            total += result;
        }
        Console.WriteLine("Total: " + total);

        Console.WriteLine("Retrieving User objects with list of IDs...");
        IEnumerable<int> ids = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
        var users = await GetUsersAsync(ids);
        foreach (User? user in users)
        {
            Console.WriteLine($"{user.id}: isEnabled={user.isEnabled}");
        }

        Console.WriteLine("Application ending.");
    }
}

// Example output:
//
// Application started.
// Counting '.NET' phrase in websites...
// https://learn.microsoft.com: 0
// https://learn.microsoft.com/aspnet/core: 57
// https://learn.microsoft.com/azure: 1
// https://learn.microsoft.com/azure/devops: 2
// https://learn.microsoft.com/dotnet: 83
// https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio: 31
// https://learn.microsoft.com/education: 0
// https://learn.microsoft.com/shows/net-core-101/what-is-net: 42
// https://learn.microsoft.com/enterprise-mobility-security: 0
// https://learn.microsoft.com/gaming: 0
// https://learn.microsoft.com/graph: 0
// https://learn.microsoft.com/microsoft-365: 0
// https://learn.microsoft.com/office: 0
// https://learn.microsoft.com/powershell: 0
// https://learn.microsoft.com/sql: 0
// https://learn.microsoft.com/surface: 0
// https://dotnetfoundation.org: 16
// https://learn.microsoft.com/visualstudio: 0
// https://learn.microsoft.com/windows: 0
// https://learn.microsoft.com/xamarin: 6
// Total: 238
// Retrieving User objects with list of IDs...
// 1: isEnabled= False
// 2: isEnabled= False
// 3: isEnabled= False
// 4: isEnabled= False
// 5: isEnabled= False
// 6: isEnabled= False
// 7: isEnabled= False
// 8: isEnabled= False
// 9: isEnabled= False
// 0: isEnabled= False
// Application ending.

Inne zasoby