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 Main Поток
    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 для долгих операций (файлы, сеть).
  • В WebAPI/ASP.NET Task помогает не тратить потоки на ожидание сети/БД, повышая производительность.
  • Организуйте "параллельное" выполнение: одновременно скачать, обработать и сохранить.
  • Почти все долгие методы имеют Async-вариант: File.ReadAllTextAsync, HttpClient.GetStringAsync и др.

FAQ и неожиданные моменты

Вопрос: Почему Task иногда выполняется синхронно?
Ответ: Если операция уже завершена (например, результат кеширован), компилятор и/или планировщик может завершить метод в том же потоке синхронно. Это нормально и ускоряет повторные вызовы.

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

Вопрос: Можно ли запустить несколько задач и дождаться только одной?
Ответ: Да — используйте Task.WhenAny.

2
Задача
C# SELF, 60 уровень, 0 лекция
Недоступна
Использование Task<TResult> для возвращения результата
Использование Task<TResult> для возвращения результата
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ