async и await
Проблем
Три заявки по 300 ms
Страницата на продукт в онлайн магазина пита склада за цената. Складът е друга програма и отговаря за 300 ms. Ако програмата стои и чака с празни ръце, в тези 300 ms не прави нищо друго. Уеб сървър с 1000 потребители, който чака така за всеки, спира: всички се редят на опашка зад един бавен склад.
Количката с три продукта е още по-зле. Трите заявки една след друга са 900 ms, а складът би отговорил и на трите наведнъж за 300 ms.
Идеята
Задача вместо чакане
Когато в кафенето поръчаш на касата, не стоиш на гишето, докато ти направят кафето. Взимаш номерче и правиш друго, а когато кафето е готово, го взимаш.
В C# номерчето е Task (задача): обект за работа, която ще свърши по-късно.
Task<decimal>е задача, която накрая ще дадеdecimal, например цена.Taskбез тип само казва „готово“.- Методът, който чака нещо, се отбелязва с
asyncи връща задача. По правило името му завършва наAsync. awaitпред задачата значи „чакай резултата, но без да спираш програмата“. Докато задачата не е готова, програмата може да върши друга работа, например да приеме следващия клиент.
Виж го
async и await
Складът е клас с един метод. Task.Delay(300) е задача, която свършва след 300 ms. Тук тя играе бавния склад:
class Warehouse{ static Dictionary<string, decimal> prices = new Dictionary<string, decimal> { { "Еспресо на зърна", 24.90m }, { "Зелен чай сенча", 9.80m }, { "Мента", 4.20m }, };
public static async Task<decimal> GetPriceAsync(string product) { await Task.Delay(300); return prices[product]; }}Програмата пита за цената и я чака с await:
Console.WriteLine("Питам за цената...");decimal price = await Warehouse.GetPriceAsync("Еспресо на зърна");Console.WriteLine("Цена: " + price + " €");Питам за цената...Цена: 24.90 €async Task<decimal>: методът е асинхронен и накрая даваdecimal. В тялото пишешreturn 24.90m, а неreturnна задача: задачата прави компилаторът.awaitможе да се пише само вasyncметод или в top-level кода на програмата.await Warehouse.GetPriceAsync(…)дава направоdecimal. Задачата се „разопакова“.
По-точно, за любопитните
- С обвивката
class ProgramметодътMainсъщо става асинхронен:static async Task Main(). Top-level кодът сawaitе съкратен запис на точно това.
class Program{ static async Task Main() { decimal price = await Warehouse.GetPriceAsync("Мента"); Console.WriteLine("Цена: " + price + " €"); }}Цена: 4.20 €- Истинската работа, която се чака, е мрежа, диск или база данни:
HttpClient(урокът „Първият ти сървър“),File.ReadAllTextAsync, заявки към база.Task.Delayсамо я замества тук, за да работи кодът без мрежа.
Пробвай
В какъв ред
Какво ще отпечата програмата?
Console.WriteLine("Поръчката е приета");Task<decimal> priceTask = GetPriceAsync();Console.WriteLine("Чакам цената");decimal price = await priceTask;Console.WriteLine("Цена: " + price + " €");
static async Task<decimal> GetPriceAsync(){ Console.WriteLine("Питам склада"); await Task.Delay(300); Console.WriteLine("Складът отговори"); return 24.90m;}Async методът започва веднага и върви до първия await на нещо незавършено: печата „Питам склада“. Там връща задачата, затова следва „Чакам цената“. await priceTask чака 300 ms, после методът продължава с „Складът отговори“ и връща цената.
Виж го
Една след друга или едновременно
Количката има три продукта:
List<string> products = new List<string> { "Еспресо на зърна", "Зелен чай сенча", "Мента" };С await в цикъла всяка заявка чака предишната:
// Една след друга: около 900 ms.foreach (string product in products){ decimal price = await Warehouse.GetPriceAsync(product); Console.WriteLine(product + ": " + price + " €");}Еспресо на зърна: 24.90 €Зелен чай сенча: 9.80 €Мента: 4.20 €Ако първо пуснеш трите задачи и чак после чакаш, те вървят едновременно. Task.WhenAll е задача, която свършва, когато свършат всички, и дава резултатите им в масив, в реда на задачите:
// Едновременно: около 300 ms.List<Task<decimal>> tasks = products.Select(p => Warehouse.GetPriceAsync(p)).ToList();decimal[] prices = await Task.WhenAll(tasks);for (int i = 0; i < products.Count; i++){ Console.WriteLine(products[i] + ": " + prices[i] + " €");}Еспресо на зърна: 24.90 €Зелен чай сенча: 9.80 €Мента: 4.20 €Select от урока за LINQ вика GetPriceAsync за всеки продукт. Методът не чака, а връща задача, затова tasks е списък от три задачи, които вече вървят. Изходът е същият, но чакането е колкото най-бавната заявка, около 300 ms вместо 900.
foreach (string product in products){ decimal price = await Warehouse.GetPriceAsync(product); Console.WriteLine(product + ": " + price + " €");}
List<Task<decimal>> tasks = products.Select(p => Warehouse.GetPriceAsync(p)).ToList();decimal[] prices = await Task.WhenAll(tasks);for (int i = 0; i < products.Count; i++){ Console.WriteLine(products[i] + ": " + prices[i] + " €");}Стъпка по стъпка, с думи
- Три продукта. Складът отговаря на всяка заявка след 300 ms.
- await спира цикъла, докато складът отговори за „Еспресо на зърна“. Следващата заявка още не е тръгнала.
- await спира цикъла, докато складът отговори за „Зелен чай сенча“. Следващата заявка още не е тръгнала.
- await спира цикъла, докато складът отговори за „Мента“. Следващата заявка още не е тръгнала.
- Една след друга: 3 × 300 = 900 ms.
- Select пуска трите заявки наведнъж. Всяка връща Task веднага, без да чака.
- await Task.WhenAll чака всички задачи. Трите чакания вървят едновременно.
- След 300 ms и трите цени са готови: 300 ms вместо 900. Изходът е същият.
По-точно, за любопитните
- Задачите може да са от различен тип. Тогава
WhenAllсамо чака, а резултатите взимаш от всяка задача сawait. Задачата вече е готова, затова тозиawaitне чака:
Task<decimal> priceTask = Warehouse.GetPriceAsync("Мента");Task<int> stockTask = GetStockAsync("Мента");await Task.WhenAll(priceTask, stockTask);Console.WriteLine("Мента: " + await priceTask + " €, " + await stockTask + " бр.");
static async Task<int> GetStockAsync(string product){ await Task.Delay(300); return 14;}Мента: 4.20 €, 14 бр.- Капан:
.Resultи.Wait().task.Resultдава резултата, аtask.Wait()чака, но и двете блокират програмата, докато задачата свърши. Губиш ползата отasync, а в приложение с прозорци (WPF) или в стар ASP.NET проект програмата може да зависне завинаги: чака задача, която чака нея. Освен това изключението идва обвито вAggregateException. Пишиawait. - Капан:
async void. Методasync voidне връща задача, затова никой не може да го изчака, а изключението му не се хваща сtryоколо извикването. Пишиasync Task.async voidе само за обработчици на събития (урокът за WPF).
Пробвай
Изчакай всички
Кой метод чака всички задачи от списъка и дава резултатите им?
decimal[] prices = await Task.(tasks);Task.WhenAll(tasks) свършва, когато свършат всички задачи. С await дава масив с резултатите в реда на задачите.
Виж го
Когато складът откаже
Какао в склада няма. prices[product] хвърля KeyNotFoundException, както в урока за речниците. Изключението в async метод се пази в задачата и излиза при await. Затова се хваща, както си свикнал, с try около await:
try{ decimal price = await Warehouse.GetPriceAsync("Какао"); Console.WriteLine("Цена: " + price + " €");}catch (KeyNotFoundException){ Console.WriteLine("Няма такъв продукт");}Няма такъв продуктПо-точно, за любопитните
- Ако при
Task.WhenAllсе провалят няколко задачи,awaitхвърля първото изключение. Останалите са вtask.Exceptionна всяка задача. Задачите, които не са се провалили, имат резултат, затова можеш да ги изчакаш една по една, всяка в свойtry(задача 2 от упражнението). - Ако заявка може да продължи твърде дълго, истинският код я прекъсва след време с
CancellationToken. Ще го срещнеш вHttpClientи в уеб сървъра.
Пробвай
Грешката, която не се хвана
Програмата трябва да отпечата „Няма такъв продукт“, но спира с KeyNotFoundException. На кой ред е проблемът?
Избери реда с грешката.
Извикването на GetPriceAsync не хвърля нищо: то само връща задачата, а изключението се пази в нея. То излиза на ред 11, при await task, а там вече няма try. Поправката е await да е вътре в try: decimal price = await Warehouse.GetPriceAsync("Какао");.
Упражнение
Цените на поръчката
Реши задачите в редактора си. Ползвай класа Warehouse от урока.
1. Последователно и едновременно. Вземи цените на трите продукта веднъж една след друга и веднъж с Task.WhenAll, после отпечатай общата сума. Сложи // около 900 ms и // около 300 ms като коментари под двата начина. Ако искаш да го измериш, System.Diagnostics.Stopwatch.StartNew() пуска хронометър, а ElapsedMilliseconds казва колко е минало:
Една след друга: Еспресо на зърна: 24.90 € Зелен чай сенча: 9.80 € Мента: 4.20 €Едновременно: Еспресо на зърна: 24.90 € Зелен чай сенча: 9.80 € Мента: 4.20 €Общо: 38.90 €2. Отказана заявка. Поръчката е мента, какао и еспресо. Пусни трите заявки едновременно, после изчакай всяка в свой try. За липсващия продукт отпечатай „няма го в склада“, а сумата да е само на наличните:
Мента: 4.20 €Какао: няма го в складаЕспресо на зърна: 24.90 €За плащане: 29.10 €Тест