Programowanie asynchroniczne
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), 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 synchronizację. Jest on zgodny z wzorcem asynchronicznym ( TAP) opartym na zadaniach.
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 kluczowe async i await . 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ę
TaskTask<T>lub wewnątrzasyncmetody . - 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 awaitmetodę , która wykonała , i ostatecznie umożliwia interfejsowi użytkownika odpowiadanie lub elastyczne korzystanie z usługi. Chociaż istnieją sposoby podejścia do kodu asynchronicznego innego niż i await, ten artykuł koncentruje się na 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 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 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 zadania jest uruchomienie wątku w tle, Task.Runktóry będzie działać przy użyciu funkcji , i oczekiwanie na wynik przy użyciu funkcji await. Dzięki temu interfejs użytkownika może być bezproblemowy w przypadku 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 jasno wyraża intencję zdarzenia kliknięcia przycisku, nie wymaga ręcznego zarządzania wątkiem w tle i robi to w sposób nieblokowy.
Co się dzieje w ramach okładki
Po stronie języka C# kompilator przekształca kod w maszynę stanu, await która śledzi rzeczy, takie jak uzyskanie wykonania po osiągnięciu i wznowienie 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 z we/wy, jak i kodu powiązanego z procesorem CPU, ale w różny sposób dla każdego scenariusza.
- Kod asynchroniczny używa i
Task<T>Task, które są konstrukcjami używanymi do modelowania pracy wykonywanej w tle. - Słowo
asynckluczowe zamienia metodę w metodę asynchroniczną, która umożliwia użycie słowaawaitkluczowego w jej treści. - Zastosowanie słowa
awaitkluczowego wstrzymuje metodę wywołującą i daje kontrolę jej wywołującemu do czasu ukończenia oczekiwanego zadania. awaitMoż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 asyncawait pokazują, jak można używać funkcji i do pracy związanej z we/wy i procesorem CPU. Jest to kluczowy element, który pozwala 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 może potencjalnie prowadzić do błędnego użycia niektórych konstrukcji.
Poniżej znajdują się dwa pytania, które należy zadać przed napisaniem kodu:
Czy twój kod będzie "czekał" na coś, na przykład dane z bazy danych?
Jeśli odpowiedź brzmi "tak", oznacza to, że Praca jest powiązana z we/wy.
Czy kod będzie wykonywać kosztowne obliczenia?
Jeśli odpowiesz "tak", twoja praca jest powiązana z procesorem CPU.
Jeśli masz pracę powiązaną z we /wy, użyj i awaitawaitTask.Run. Nie należy używać biblioteki równoległej zadań.
Jeśli praca jest powiązana z procesorem CPU i zależy Ci na czasie odpowiedzi, awaitużyj i , ale odtąd odtąd praca w awaitTask.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 równoległej zadań.
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 https://dotnetfoundation.org strony głównej pod i zlicza liczbę wystąpień ciągu ".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 Task API zawiera dwie metody i Task.WhenAllTask.WhenAny, które umożliwiają pisanie kodu asynchronicznego, który wykonuje nieblokowanie oczekiwania na wiele zadań w tle.
W tym przykładzie pokazano, jak można pobrać User dane dla zestawu userIds.
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 bardziej zwięzłego pisania tego 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. Ponieważ LINQ używa wykonywania odroczonego (z opóźnieniem), foreach wywołania asynchroniczne nie będą odbywać się natychmiast, tak jak w pętli, chyba że wymusz iteruj wygenerowaną sekwencję za pomocą wywołania do .ToList() 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.
asyncasyncawaitawaitNależy o tym pamiętać. Jeśli
awaitelement nie jest używany wasynctreści metody, kompilator języka C# generuje ostrzeżenie, ale kod jest kompilowany i uruchamiany tak, jakby był 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 na .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ą one jawnie wywoływane przez kod, jawne nazewnictwo nie jest tak ważne.
async voidasync voidasync voidjest jedynym sposobem na umożliwienie działania asynchronicznych programów obsługi zdarzeń, ponieważ zdarzenia nie mają zwracanych typów (w związku z tym nie mogą używać metodTaskiTask<T>). Każde inne użycie nieasync voidjest zgodne z modelem TAP i może być trudne do użycia, takie jak:- Wyjątków zgłaszanych w
async voidmetodzie nie można przechwycić poza metodą . async voidMetody są trudne do przetestowania.async voidMetody mogą powodować złe skutki uboczne, jeśli wywołujący nie oczekuje, że są asynchroniczne.
- Wyjątków zgłaszanych w
Starannie rozważne używanie 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 blokowania może łatwo spowodować zakleszczenie, jeśli nie jest poprawnie zapisany. 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
Taskdo oczekiwania na zakończenie a może spowodować zakleszczenia i zablokowane wątki kontekstu i może wymagać bardziej 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 nieblokowy:Użyj polecenia... Zamiast tego... Gdy chcesz to zrobić... awaitTask.WaitlubTask.ResultPobieranie wyniku zadania w tle await Task.WhenAnyTask.WaitAnyOczekiwanie na ukończenie dowolnego zadania await Task.WhenAllTask.WaitAllOczekiwanie na ukończenie wszystkich zadań await Task.DelayThread.SleepOczekiwanie na okres czasu Rozważ użyciewszędzie tam, gdzie to możliwe
Zwracanie obiektu z
Taskmetod asynchronicznych może wprowadzać wąskie gardła wydajności w niektórych ścieżkach.Taskjest typem referencyjnym, więc użycie go oznacza przydzielenie obiektu. W przypadkachasync, 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 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
Często zadawane pytanie brzmi: "Kiedy należy użyć Task.ConfigureAwait(Boolean) metody?". Metoda umożliwia wystąpieniu
Taskskonfigurowanie jego await. 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 tematConfigureAwaitusługi , zobaczConfigureAwait).Pisanie mniej stanowego kodu
Nie należy zależeć od stanu obiektów globalnych ani od wykonywania 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.
- Warunków 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.