JavaRush /Курси /C# SELF /Ключові слова async і...

Ключові слова async і await

C# SELF
Рівень 59 , Лекція 2
Відкрита

1. Вступ

Перш ніж занурюватися в код, розберімося з мотивацією та уявімо типову ситуацію. Припустімо, ваша програма завантажує файл з інтернету:

// Псевдокод
var data = DownloadFile("https://example.com/file");
ProcessData(data);

Проблема в тому, що під час завантаження програма завмирає. Інші дії не виконуються: користувач не може ані рухати мишею, ані натискати кнопки — на екрані «зависле» вікно.

Раніше (і в інших мовах) для розв’язання цієї проблеми доводилося використовувати потоки (Thread), задачі (Task), делегати, таймери — усе це призводило до громіздкого коду, який важко читати й підтримувати. У C# запровадили простіший підхід: асинхронність за допомогою ключових слів async і await. Тепер можна писати асинхронний код майже так само просто, як звичайний.

Класичні труднощі без async/await

Для контрасту погляньмо, як виглядало б виконання тривалої операції за допомогою потоків, якби ми не хотіли блокувати інтерфейс:

// Приклад без async/await, вручну
var thread = new Thread(() =>
{
    var data = DownloadFile("https://example.com/file");
    Console.WriteLine("Файл завантажено!");
});
thread.Start();

Цей спосіб доволі примітивний: потрібно керувати потоками вручну, немає простого способу чекати на результат, а винятки обробляти складно.

2. Асинхронність у C#: синтаксис

Визначення: що таке async і await?

async — це модифікатор, який позначає метод як асинхронний. Такий метод зазвичай повертає або Task (або Task<T>), або ValueTask. Це обіцянка результату трохи пізніше (як замовлення в інтернет‑магазині: оформили — і чекаєте).

await — це оператор, який зупиняє виконання методу в цій точці, доки операція не завершиться, але не блокує потік: інший код може працювати далі.

Як виглядає асинхронний метод?

public async Task MyAsyncMethod()
{
    Console.WriteLine("Завантажуємо файл...");
    var data = await DownloadFileAsync("https://example.com/file"); // Чекаємо результат асинхронно!
    Console.WriteLine("Готово!");
}

Зверніть увагу:

  • До методу додано модифікатор async.
  • Усередині методу використовується await для асинхронної операції.
  • Метод повертає Task (або Task<T>, якщо є значення, що повертається).

Візуалізація: що відбувається під час виклику async-методу?

graph LR
A[Виклик MyAsyncMethod] --> B[Виконання до await]
B --> C[Виклик DownloadFileAsync]
C --> D{Очікування}
D --> |Файл не завантажено| E[Звільнення потоку]
D --> |Файл завантажено| F[Виконання після await]
F --> G[Завершення методу]
  • До першого await метод виконується синхронно.
  • На await виконання методу призупиняється, а керування повертається викликачеві.
  • Коли асинхронну операцію завершено, виконання продовжується після await — ніби нічого й не сталося.

3. Приклад: асинхронне завантаження з використанням async/await

Нехай наш навчальний застосунок тепер завантажує текст з інтернету й друкує його розмір, не блокуючи інший код.

using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
    // Асинхронний метод (повертає Task)
    public static async Task DownloadAndPrintLengthAsync(string url)
    {
        Console.WriteLine("Починаємо завантаження...");

        // Використовуємо HttpClient — він підтримує асинхронні методи
        using (var client = new HttpClient())
        {
            string data = await client.GetStringAsync(url);
            Console.WriteLine($"Завантаження завершено! Довжина тексту: {data.Length} символів.");
        }

        Console.WriteLine("Роботу методу завершено.");
    }

    static void Main()
    {
        // Запускаємо асинхронну операцію і чекаємо її завершення
        var task = DownloadAndPrintLengthAsync("https://www.example.com");
        task.Wait(); // Для простих консольних прикладів це допустимо. У реальних UI або веб-застосунках цей виклик блокує потік і може спричинити взаємні блокування (deadlock).
    }
}

Пояснення:

  • DownloadAndPrintLengthAsync — повністю асинхронний завдяки async і await.
  • Усередині методу очікуємо завершення асинхронного завантаження рядка за допомогою await.
  • У Main() ми запускаємо задачу й явно чекаємо її завершення через Wait(). У сучасному C# можна зробити й сам Main асинхронним (async Task Main) і писати просто await.

4. Як це працює?

Різниця між синхронним та асинхронним кодом

СИНХРОННИЙ ВАРІАНТ

Console.WriteLine("Початок");
string data = client.GetStringAsync(url).Result; // .Result блокує потік!
Console.WriteLine("Операцію завершено");

АСИНХРОННИЙ ВАРІАНТ

Console.WriteLine("Початок");
string data = await client.GetStringAsync(url); // Потік не блокується
Console.WriteLine("Операцію завершено");

Як працює await «під капотом»?

Коли ви використовуєте await, C# автоматично «розбиває» метод на дві (або більше) частини: усе, що до await, і все, що після. Коли викликаний асинхронний метод повертає Task, ваш метод повертає керування викликачеві, а після завершення Task виконання продовжується після await. Усе це відбувається автоматично — не потрібно самостійно керувати потоками чи перемиканням контекстів.

Цікавий факт: C# «перетворює» асинхронний метод на машину станів, де кожен «await» — це нова «точка повернення».

Використання async/await у реальних застосунках

static async Task Main(string[] args)
{
    var downloadTask = DownloadAndPrintLengthAsync("https://www.example.com");
    // Поки йде завантаження, робимо щось ще
    for (int i = 0; i < 5; i++)
    {
        Console.WriteLine($"Працюємо... ітерація {i}");
        await Task.Delay(500); // Пауза на 0,5 с, імітуємо роботу
    }
    await downloadTask; // Дочекаємося завершення завантаження
}

Тепер програма і завантажує, і не блокується: «жива» багатозадачність — майже як у кота, що одночасно спить і стежить за мискою.

Асинхронність ≠ багатопоточність

Важлива відмінність, яку часто плутають: асинхронний код може виконуватися на тому самому потоці. Асинхронність — це про відсутність блокувань потоку, а не про обов’язкове створення нових потоків. Потоки ОС — вартісний ресурс. Асинхронність дозволяє звільнити потік, не чекаючи тривалих операцій (мережевих, файлових, таймерних тощо).

Таблиця порівняння: коли що обирати?

Сценарій Thread/Task async/await
CPU-bound задачі Так Так (через Task.Run)
I/O-bound задачі Неефективно Ідеально
Багато паралелізму Складно Просто
Простота коду Складно Легко читати

5. Корисні нюанси

Асинхронний Main

У сучасних версіях C# можна зробити і метод Main асинхронним!

static async Task Main(string[] args)
{
    // Весь ваш асинхронний код можна очікувати через await прямо в Main
    await DownloadAndAnalyzeFileAsync("https://example.com/file");
}

Типова помилка: забули await — задача «втекла»

SomeAsyncFunction(); // без await — ніхто не очікує завершення!

У результаті задача виконуватиметься «у нікуди», і якщо станеться виняток, ви про це навіть не дізнаєтеся.

Коли НЕ варто писати async-методи

  • Якщо всередині методу немає жодної асинхронної операції (жодного await), не позначайте його як async.
  • Не пишіть методи з async void (виняток — обробники подій).

Короткі правила і типові питання

  • Чи можна використовувати await поза async-методом? Ні. Лише всередині методів, позначених як async.
  • Чи може бути кілька await в одному методі? Так, скільки завгодно — кожен є «точкою очікування».
  • Що, якщо потрібно повернути результат? Використовуйте Task<T> і пишіть return.
  • Чи можна комбінувати кілька асинхронних задач? Звісно. Можна запускати кілька задач паралельно й очікувати їх усі через Task.WhenAll.

6. Типові помилки під час використання async/await

Помилка №1: «вогняна куля» — без await після виклику асинхронної задачі.

DownloadAndPrintLengthAsync("https://www.example.com");
Console.WriteLine("Усе виконано!"); // Насправді завантаження ще триває!

Код не дочекається завершення асинхронної операції. Усі асинхронні задачі потрібно або очікувати через await, або явно використовувати Wait() (але другий спосіб небезпечний і може призвести до блокувань).

Помилка №2: змішування синхронного й асинхронного коду через .Result або .Wait().

Це антипатерн, який зводить нанівець усі переваги асинхронності. В UI або ASP.NET‑застосунках це майже гарантовано призведе до взаємного блокування (deadlock): асинхронна задача чекає на звільнення потоку, а потік заблокований в очікуванні завершення цієї задачі. Запам’ятайте мантру: «async all the way» (асинхронність до самого верху).

Помилка №3: використання async void.

Методи async void не можна очікувати (await), а винятки, викинуті з них, не перехоплюються стандартним try-catch і зазвичай призводять до аварійного завершення всього застосунку. Єдине припустиме застосування async void — обробники подій (наприклад, async void Button_Click(...)), де цього вимагає сигнатура. В усіх інших випадках використовуйте async Task.

Помилка №4: зайвий async у методі без await.

Якщо ви позначаєте метод як async, але не використовуєте всередині нього await, компілятор видасть попередження. Такий код виконуватиметься повністю синхронно, але з додатковими накладними витратами на створення зайвої «машини станів». Це вводить в оману й знижує продуктивність.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ