Към съдържанието
wannacode

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;
}
Отговор

Виж го

Една след друга или едновременно

Количката има три продукта:

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] + " €");
}
Стъпка по стъпка, с думи
  1. Три продукта. Складът отговаря на всяка заявка след 300 ms.
  2. await спира цикъла, докато складът отговори за „Еспресо на зърна“. Следващата заявка още не е тръгнала.
  3. await спира цикъла, докато складът отговори за „Зелен чай сенча“. Следващата заявка още не е тръгнала.
  4. await спира цикъла, докато складът отговори за „Мента“. Следващата заявка още не е тръгнала.
  5. Една след друга: 3 × 300 = 900 ms.
  6. Select пуска трите заявки наведнъж. Всяка връща Task веднага, без да чака.
  7. await Task.WhenAll чака всички задачи. Трите чакания вървят едновременно.
  8. След 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);

Виж го

Когато складът откаже

Какао в склада няма. 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. На кой ред е проблемът?

Избери реда с грешката.

Упражнение

Цените на поръчката

Реши задачите в редактора си. Ползвай класа 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 €

Тест

Провери се

1. Какво връща метод async Task<decimal> GetPriceAsync(), когато го извикаш без await?
2. Три заявки по 300 ms се пускат с Task.WhenAll. Около колко продължава чакането?
3. Async метод хвърля изключение след await Task.Delay(300). Къде го хващаш?
4. Защо task.Result е капан?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·