Модуль 6

Tasks: структура та best practices

Ось готовий урок у стилі CS50, адаптований під тему асинхронних задач (Tasks) у C#/.NET (оскільки саме там термін "Task" є стандартом де-факто, але принципи універсальні для JS Promises чи Python AsyncIO).


🎓 CS50: Tasks, Асинхронність та Best Practices

Привіт, друзі! 👋

Сьогодні ми говоримо про тему, яка перетворює "гальмівні" програми на швидкі та чутливі. Ми говоримо про Tasks (Задачі) та асинхронність.


1. 🔥 Вступ: Чому ми не можемо просто чекати?

Уявіть, що ви прийшли в популярну кав'ярню вранці. Ви підходите до каси, замовляєте складне лате і тост з авокадо.

Ситуація А (Синхронна / Блокуюча): Касир приймає замовлення, йде до кавомашини, 2 хвилини варить каву, потім йде різати авокадо, смажить хліб... А ви стоїте і чекаєте біля каси. І, що найгірше, весь цей час черга за вами не рухається. Касир заблокований вашим замовленням.

Ситуація Б (Асинхронна / Tasks): Касир приймає замовлення, дає вам пейджер (або чек з номером) і каже: "Я повідомлю, коли буде готово". Він миттєво перемикається на наступного клієнта. Тим часом бариста (інший ресурс) робить каву.

Питання до вас: У якому випадку кав'ярня обслуговує 100 людей швидше? Звісно, у другому.

У програмуванні Task — це і є той "пейджер" або "обіцянка" результату. Без Tasks ваш інтерфейс зависає, доки вантажиться файл, а сервер не може обробити запит іншого користувача, бо чекає відповіді від бази даних.

Навіщо це нам? Щоб не марнувати час (і ресурси процесора) на пусте очікування.


2. 🧠 Теоретична база: Що там "під капотом"?

Давайте розберемо це без складних академічних слів.

Що таке Task?

Task — це об'єкт, який представляє роботу, що виконується (або буде виконана) десь у майбутньому. Це не сама робота, а "квитанція" на неї.

Як це працює? (Спрощено)

Коли ви запускаєте "важку" операцію (наприклад, запит до бази даних) синхронно, ваш потік (Thread) просто зупиняється і "спить", чекаючи відповіді. Це дорого! Потоки жеруть пам'ять.

Коли ви використовуєте async/await: 1. Ваш код доходить до await. 2. Він бачить: "Ага, тут треба почекати, поки база відповість". 3. Замість того, щоб блокувати потік, система звільняє його. Цей потік біжить робити іншу корисну роботу (обслуговувати інший HTTP-запит, малювати UI). 4. Коли база даних каже "Я все!", система хапає вільний потік і продовжує виконання вашого коду з того місця, де він зупинився.

🔑 Ключові слова (Запам'ятай це!)

  1. async: Маркер методу. Каже компілятору: "Всередині буде асинхронність, перепиши цей метод по-розумному".
  2. Task / Task<T>: Тип, що повертає метод. Це "обіцянка" повернути результат типу T (або просто виконатися, якщо Task без типу).
  3. await: Магічна команда. Означає: "Тут я віддаю керування, поки задача не завершиться. Розбудіть мене, коли буде результат".

Інтуїтивно: Task — це як замовлення піци. Ви подзвонили (запустили Task), поклали слухавку і займаєтесь своїми справами (код виконується далі або потік звільняється). Коли кур'єр дзвонить у двері (await завершився), ви забираєте піцу (результат).


3. 🧪 Приклади: Від простого до реального

Приклад 1: "Імітація бурхливої діяльності"

Давайте просто почекаємо 1 секунду.

// Синхронно (Погано 🛑)
public void SleepyMethod()
{
    Thread.Sleep(1000); // Потік заморожено! Інтерфейс висить!
    Console.WriteLine("Прокинувся");
}

// Асинхронно (Добре ✅)
public async Task AwakeMethodAsync()
{
    await Task.Delay(1000); // Потік вільний!
    Console.WriteLine("Прокинувся, а система працювала далі");
}

Приклад 2: Реальний кейс — Завантаження даних

Уявіть, що нам треба завантажити дані користувача з бази.

Питання: Що буде, якщо ми приберемо слово await перед GetUserAsync()? Ви отримаєте не користувача, а сам об'єкт Task. Ви спробуєте працювати з "квитанцією на піцу" замість самої піци.

public async Task<User> GetUserDataAsync(int id)
{
    // Імітуємо довгий запит до БД
    Console.WriteLine("Запит до бази...");

    // Тут потік звільняється на 2 секунди!
    await Task.Delay(2000); 

    Console.WriteLine("Дані отримано!");
    return new User { Name = "David", Id = id };
}

// Використання:
public async Task ShowUserAsync()
{
    User user = await GetUserDataAsync(1); // Чекаємо на результат, не блокуючи потік
    Console.WriteLine($"Привіт, {user.Name}");
}

Приклад 3: Паралельне виконання (Турбо-режим 🚀)

Припустимо, нам треба завантажити дані трьох різних користувачів. Новачки роблять це послідовно: завантажив першого (2 сек), потім другого (2 сек), потім третього (2 сек). Разом = 6 секунд.

А можна швидше?

public async Task LoadAllUsersFastAsync()
{
    // 1. Ми ЗАПУСКАЄМО задачі, але ще не чекаємо їх завершення!
    // Вони полетіли виконуватися паралельно (якщо є ресурси).
    Task<User> task1 = GetUserDataAsync(1);
    Task<User> task2 = GetUserDataAsync(2);
    Task<User> task3 = GetUserDataAsync(3);

    // 2. Тепер чекаємо, поки ВСІ вони завершаться.
    // Це займе всього ~2 секунди (час найдовшої задачі).
    await Task.WhenAll(task1, task2, task3);

    Console.WriteLine("Всі користувачі завантажені!");
}

Замість черги ми запустили трьох кур'єрів одночасно.


4. 🛠 Практична частина

Час кодити! Якщо немає під рукою IDE, пиши логіку на папері або в голові.

Завдання 1: Hello Async Напиши метод PrintMessageAsync, який виводить "Start", чекає 3 секунди (асинхронно!), а потім виводить "End". Виклич його.

Завдання 2: The Breakfast Challenge У тебе є методи: FryEggsAsync() (3 сек) і ToastBreadAsync() (2 сек). Напиши код, який готує сніданок за 3 секунди, а не за 5. (Підказка: Task.WhenAll).

Завдання 3: "А що, якщо..." (Помилка) Що станеться, якщо в асинхронному методі станеться помилка (Exception)? Напиши try-catch блок навколо виклику await Task.Delay(...), де всередині методу кидається помилка. Чи спіймає її catch?

Завдання 4: Міні-кейс Ти розробляєш функцію відправки email-розсилки 100 користувачам. SendEmailAsync(string email) займає 1 секунду. Як відправити їх так, щоб не чекати 100 секунд, але й не "покласти" поштовий сервер, відправивши 100 листів одночасно в одну мілісекунду? (Це завдання із зірочкою — подумай про Task.WhenAll порціями або семафори).


5. 💡 Мислення як у розробника

Ось де новачки "ламають зуби", а сеньйори посміхаються.

⚠️ Типова пастка №1: Task.Result або .Wait()

Ніколи, чуєте, НІКОЛИ не використовуйте .Result або .Wait() для асинхронних задач у синхронному коді (особливо в UI або ASP.NET). Це призводить до Deadlock (Смертельні обійми). Аналогія: Ви дзвоните кур'єру і кажете "Я не покладу слухавку, поки ти не приїдеш", а кур'єр не може зайти у під'їзд, бо ви не кладете слухавку, щоб відкрити йому домофон.Рішення: Завжди використовуйте await.

⚠️ Типова пастка №2: async void

Використовуйте async void тільки для обробників подій (Event Handlers) (наприклад, кнопка Click). У всіх інших випадках — async Task. Чому? Тому що async void — це "вистрелив і забув". Якщо там станеться помилка, ваша програма впаде, і ви навіть не дізнаєтесь чому (catch її не спіймає).

🧠 Як думає профі:

Досвідчений розробник бачить дерево викликів. Він знає правило: "Async all the way down" (Асинхронність до самого низу). Якщо ви зробили метод асинхронним, то і той, хто його викликає, має бути асинхронним. Не розривайте цей ланцюжок.


6. 🧩 Підсумок

Отже, що ми сьогодні забрали з собою:

  1. Tasks дозволяють робити кілька справ одночасно, не блокуючи основний потік виконання.
  2. await — це не "зупинись і стій", а "звільни потік і повернись, коли буде готово".
  3. Task.WhenAll — ваш найкращий друг для паралельного виконання незалежних задач.
  4. Синхронний код у 2024 році для операцій вводу/виводу (мережа, диск, БД) — це моветон.

Тепер ви вмієте: перетворювати повільний послідовний код на ефективний асинхронний конвеєр.

👀 Тизер наступного уроку: Ми навчилися запускати задачі. Але що робити, якщо задача зависла або користувач натиснув "Скасувати"? На наступному уроці розберемо CancellationTokens — як ввічливого просити задачу "закруглятися".

This was CS50 Tasks. Побачимось! 🎬