JavaRush /Курси /C# SELF /Класи Task і

Класи Task і Task<TResult>

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

1. Вступ

У світі асинхронного програмування ми живемо за принципом «делегування із зворотним зв’язком». Уявіть: вам потрібно завантажити величезний файл, проаналізувати гігабайти логів або надіслати запит на далекий сервер. Замість того, щоб завмерти в очікуванні, мов статуя, ми кажемо системі: «Займися цим, а я поки попрацюю над чимось іншим. Коли закінчиш — неодмінно дай знати!»

Саме тут на сцену виходить Task — елегантне втілення такого підходу. Це не просто технічна абстракція, а своєрідний «розумний посередник», який бере на себе виконання роботи й гарантує, що результат не загубиться в цифровій порожнечі.

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

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

Клас Task

Task — це базовий будівельний блок асинхронного програмування в .NET. Він представляє запущену або майбутню операцію, результат якої з’явиться згодом. Якщо метод нічого не має повертати, використовуємо просто Task.

public async Task BackupToCloudAsync()
{
    // Робить магію резервного копіювання, нічого не повертає
}

Клас Task<TResult>

Якщо потрібно повернути результат (наприклад, рядок, число, об’єкт…), використовуємо Task<TResult>:

public async Task<string> DownloadHtmlAsync(string url)
{
    // Завантажує сторінку й повертає HTML-код
    return "<html>...</html>";
}

Чому Task, а не Thread?

Thread керує самим потоком (це складно й небезпечно), а Task — вищорівнева абстракція: він може виконуватися в пулі потоків, може працювати асинхронно без виділення нового потоку (наприклад, під час I/O‑операцій) і не змушує вас перейматися низькорівневими деталями.

Клас Task дає змогу описати: «Я хочу запустити цю дію», а вже як саме вона виконуватиметься — нехай вирішує .NET!

2. Влаштування об’єкта Task

Властивості та методи Task, які варто знати

Властивість / Метод Опис
Status
Поточний стан завдання
Result
Результат для Task<TResult> (блокує потік)
IsCompleted
Чи завершене завдання
IsFaulted
Чи сталася помилка (виняток) у завданні
IsCanceled
Чи було завдання скасовано
Wait()
Блокує поточний потік до завершення (небезпечно)
ContinueWith()
Запустити ще одне завдання після завершення
Exception
Доступ до винятку, якщо Task завершилося з помилкою
Id
Унікальний ідентифікатор завдання

Як працює асинхронний метод із Task

sequenceDiagram
    participant Main as Головний потік
    participant Task as Task (Фонове завдання)
    Main->>Task: Запуск Task.Run(() => ...)
    Note right of Task: Виконання у фоні
(CPU або I/O) alt Завдання завершене Task->>Main: await завершився — рухаємося далі else Помилка Task->>Main: await викидає виняток end

3. Створення й запуск завдань: як працює Task

Асинхронні методи з async

Найпоширеніший випадок — ви просто оголошуєте метод як async і повертаєте або Task, або Task<TResult> (як щойно бачили).

Task.Run: виконання в пулі потоків

Якщо потрібно виконати важку роботу у фоновому завданні (наприклад, порахувати великі числа або закодувати відео), можна використати Task.Run:

Task work = Task.Run(() =>
{
    // Складні обчислення — не блокуємо основний потік!
    Console.WriteLine("Розпочато фонові обчислення...");
    Thread.Sleep(2000); // Емуляція тривалої роботи
    Console.WriteLine("Фонові обчислення завершено!");
});

Якщо потрібно отримати результат:

Task<int> calculateTask = Task.Run(() =>
{
    // Наприклад, підрахунок суми перших 100 чисел
    int sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    return sum;
});

Task.Factory.StartNew

Це більш низькорівневий і гнучкий спосіб, що дає змогу тонко налаштовувати запуск завдання (наприклад, вказувати планувальник, передавати параметри тощо). У сучасному коді зазвичай радять використовувати Task.Run, бо він простіший і краще убезпечує від помилок.

4. Застосунок дня: наш довідник книжок

Припустімо, у нас є застосунок‑довідник книжок і потрібно додати функцію завантаження книжок із «хмарного» джерела — це буде I/O‑bound операція (повільний HTTP‑запит або читання з файла).

Додамо метод, який асинхронно «завантажує» книжки (емуляція затримки):

public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
}

public class BookCatalog
{
    public List<Book> Books { get; set; } = new();

    public async Task LoadBooksAsync()
    {
        Console.WriteLine("Завантаження книжок...");
        await Task.Delay(2000); // Емуляція тривалого завантаження (наприклад, HTTP або файл)
        Books = new List<Book>
        {
            new Book { Title = "CLR via C#", Author = "Jeffrey Richter" },
            new Book { Title = "C# in Depth", Author = "Jon Skeet" }
        };
        Console.WriteLine("Книжки успішно завантажено.");
    }
}

У Main викличемо асинхронне завантаження (через await):

var catalog = new BookCatalog();
await catalog.LoadBooksAsync();
Console.WriteLine($"У каталозі {catalog.Books.Count} книжок.");

Таблиця: Основні способи створення й запуску Task

Спосіб створення Як застосовується Результат Застосування
async-метод
async Task / async Task<T>
Асинхронна операція Переважно для I/O; зручно
Task.Run
Task.Run(() => { ... })
Фонове завдання CPU-bound (обчислення)
TaskCompletionSource<T>
Вручну створюємо й завершуємо Task Під повним контролем розробника Рідко; для низькорівневих випадків

5. Життєвий цикл Task

Task може перебувати в різних станах:

  • Created — завдання створено, але не запущено (для Task з явним запуском).
  • WaitingToRun — очікує в черзі пулу потоків.
  • Running — виконується.
  • WaitingForActivation — очікує запуску або зовнішньої активації.
  • RanToCompletion — успішно завершилося.
  • Faulted — завершилося з помилкою (винятком).
  • Canceled — скасовано (якщо підтримується скасування).

Діаграма

flowchart LR
    Start -->|Запуск завдання| Running
    Running -->|Успішно| Completed
    Running -->|Помилка| Faulted
    Running -->|Скасування| Canceled

Перевіримо це на практиці

Task task = Task.Run(() =>
{
    Thread.Sleep(1000);
});
Console.WriteLine(task.Status); // Зазвичай: Running або WaitingToRun
await task;
Console.WriteLine(task.Status); // RanToCompletion після завершення

6. Як отримати результат із Task<TResult>?

Task<TResult> — це обгортка над результатом, який з’явиться в майбутньому. Коли потрібно дочекатися результату, використовуйте await:

Task<int> sumTask = Task.Run(() =>
{
    int sum = 0;
    for (int i = 1; i <= 5; i++) sum += i;
    return sum;
});

int result = await sumTask;
Console.WriteLine(result); // 15

Якщо ви забудете написати await, отримаєте Task (обіцянку), а не результат. Це типова «асинхронна пастка».

Альтернатива: Синхронне отримання результату (НЕ РОБІТЬ В UI!)

Іноді (наприклад, у тестах) потрібно отримати результат без await. Можна використати властивість .Result:

int result = sumTask.Result;

Але якщо Task ще не завершено, цей код блокує потік, і якщо це UI‑потік, то застосунок зависне! Тому краще завжди надавати перевагу await.

Типові помилки з Task і Task<TResult>

Забули повернути Task, метод став void. Якщо метод нічого не повертає — повертайте Task, тільки не void, інакше не вийде обробити помилку.

Ігнорування await. Просто викликали метод, не дочекавшись результату, — і завдання починає жити власним життям («fire and forget»). Ви вже не дізнаєтеся, коли воно завершилося або впало з помилкою.

Блокувальне очікування через .Result або .Wait(). Легко отримати deadlock, особливо в UI та ASP.NET. Користуйтеся лише await.

7. Розширені можливості Task

Ланцюжок завдань: ContinueWith

Можна «навісити» дії, які потрібно виконати після завершення завдання, через ContinueWith:

Task.Run(() => 10)
    .ContinueWith(t =>
    {
        Console.WriteLine($"Виконано! Результат: {t.Result}");
    });

Але в сучасному C# зазвичай роблять це за допомогою async/await — так читати легше.

Приклад: Паралельне й послідовне завантаження даних

Припустімо, вам потрібно завантажити дві книжки з різних джерел. Можна запустити обидва Task паралельно й дочекатися обох:

public async Task LoadBooksFromMultipleSourcesAsync()
{
    Task<List<Book>> t1 = LoadFromCloudAsync();
    Task<List<Book>> t2 = LoadFromLocalAsync();

    // Очікуємо обидва завдання паралельно
    await Task.WhenAll(t1, t2);

    // Об'єднуємо результати
    Books = t1.Result.Concat(t2.Result).ToList();
}

private async Task<List<Book>> LoadFromCloudAsync()
{
    await Task.Delay(2000); // "Хмара"
    return new List<Book> { new Book { Title = "Cloud Book", Author = "Cloud Author" } };
}

private async Task<List<Book>> LoadFromLocalAsync()
{
    await Task.Delay(1000); // "Локальний диск"
    return new List<Book> { new Book { Title = "Local Book", Author = "Local Author" } };
}

Зверніть увагу: завдяки await Task.WhenAll(...) обидва запити стартують одночасно та виконуються паралельно (якщо це можливо), а застосунок чекає, поки вони обидва завершаться.

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

Task і fire-and-forget

Інколи хочеться запустити завдання й не чекати, коли воно завершиться (наприклад, надіслати логи в хмару або «підсмажити тост», доки користувач працює):

async void LogToCloudAsync(string message)
{
    await Task.Run(() =>
    {
        // Тривале надсилання логу
        Thread.Sleep(1000);
        Console.WriteLine($"Лог надіслано: {message}");
    });
}

Але пам’ятайте: якщо в такому завданні станеться помилка — дізнатися про це буде дуже складно. Тому, якщо є можливість, повертайте Task і принаймні логуйте винятки всередині!

Task і Task<TResult> у реальному житті

  • У клієнтських UWP/WPF/WinForms‑застосунках не блокуйте UI — використовуйте Task для тривалих операцій (файли, мережа).
  • У Web API/ASP.NET Task допомагає не витрачати потоки на очікування мережі/БД, підвищуючи продуктивність.
  • Організуйте «паралельне» виконання: одночасно завантажити, обробити й зберегти.
  • Майже всі тривалі методи мають Async‑варіант: File.ReadAllTextAsync, HttpClient.GetStringAsync тощо.

FAQ і неочікувані моменти

Питання: Чому Task інколи виконується синхронно?
Відповідь: Якщо операцію вже завершено (наприклад, результат кешовано), компілятор або планувальник може завершити метод у тому самому потоці синхронно. Це нормально і пришвидшує повторні виклики.

Питання: Чому не можна використовувати async void?
Відповідь: Такий метод не можна «почекати», не можна спіймати його помилки й відстежити завершення. Використовуйте Task, а async void — лише для EventHandler (наприклад, Button_Click).

Питання: Чи можна запустити кілька завдань і дочекатися лише однієї?
Відповідь: Так — використовуйте Task.WhenAny.

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