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
lubTask<T>
wewnątrz niejasync
. - 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 await
metodę , 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.Run
polecenia 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>
iTask
, 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 kluczowegoawait
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:
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.
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 await
bez Task.Run
. 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ą polecenia Task.Run
. 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 userId
s.
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.
async
metody muszą miećawait
słowo kluczowe w ich treści lub nigdy nie przyniesie!Ważne jest, aby pamiętać o tym. Jeśli
await
nie jest używana w treściasync
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 void
powinny 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 zTask
czym nie można używać elementów iTask<T>
). Każde inne użycie elementu nie jest zgodne zasync 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.
- Wyjątki zgłoszone w metodzie
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
lubTask.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życie
ValueTask
tam, gdzie to możliweZwracanie
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ż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 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 naConfigureAwait
temat 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/maui"
};
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/maui: 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.