Что такое метод ContinueWith у Task?

Ответ

ContinueWith — это метод класса Task, который позволяет создать продолжение (continuation): задачу, которая автоматически выполняется после завершения исходной задачи (предшественника), независимо от ее статуса (успех, ошибка, отмена).

Базовый синтаксис:

Task firstTask = Task.Run(() => DoWork());
Task continuationTask = firstTask.ContinueWith(previousTask =>
{
    // Этот код выполнится после завершения firstTask
    Console.WriteLine($"Первая задача завершена со статусом: {previousTask.Status}");
});

Зачем это нужно? Для организации цепочек асинхронных операций, где каждая следующая зависит от результата или факта завершения предыдущей, без блокировки потока ожиданием (Wait или Result).

Ключевые параметры TaskContinuationOptions: С их помощью можно точно контролировать, когда и при каких условиях должно запуститься продолжение.

Task.Run(() => ParseData())
    .ContinueWith(prevTask =>
    {
        // Выполнится ТОЛЬКО если первая задача завершилась успешно (RanToCompletion)
        ProcessResult(prevTask.Result);
    }, TaskContinuationOptions.OnlyOnRanToCompletion)
    .ContinueWith(prevTask =>
    {
        // Выполнится ТОЛЬКО если первая задача была отменена (Canceled)
        LogCancellation();
    }, TaskContinuationOptions.OnlyOnCanceled)
    .ContinueWith(prevTask =>
    {
        // Выполнится ТОЛЬКО если в первой задаче было исключение (Faulted)
        // prevTask.Exception содержит AggregateException
        LogError(prevTask.Exception?.InnerException);
    }, TaskContinuationOptions.OnlyOnFaulted);

Важные особенности и современная альтернатива:

  1. Обработка исключений: Исключение из предшественника не приведет к краху продолжения. Оно "упаковывается" в свойство Exception задачи-продолжения (тип AggregateException).
  2. Планировщик задач: Можно указать TaskScheduler (например, TaskScheduler.FromCurrentSynchronizationContext() для возврата в UI-поток).
  3. Современная альтернатива — async/await: В большинстве случаев код с ContinueWith менее читаем. Использование async/await предпочтительнее.

    Старый стиль с ContinueWith:

    Task.Factory.StartNew(() => "http://example.com/data.json")
        .ContinueWith(prev => DownloadStringAsync(prev.Result))
        .Unwrap() // Распаковываем вложенную задачу
        .ContinueWith(prev => JsonConvert.DeserializeObject<Data>(prev.Result));

    Современный стиль с async/await:

    public async Task<Data> GetDataAsync()
    {
        string url = await Task.Run(() => "http://example.com/data.json");
        string json = await DownloadStringAsync(url);
        return JsonConvert.DeserializeObject<Data>(json);
    }

Когда все еще полезен ContinueWith?

  • При работе с динамически создаваемыми цепочками задач.
  • Для низкоуровневой настройки поведения продолжения (например, ExecuteSynchronously для выполнения в том же потоке).
  • В библиотечном коде, где нельзя использовать async/await.

Ответ 18+ 🔞

А, ну так, ContinueWith... Это ж типа такая штука, которая говорит: «Эй, чувак, как только ты там закончишь свои делишки, не важно, чем это кончилось — успехом, пиздецом или тебя просто отменили — сделай вот это вот сразу после». Как прицеп к поезду, только для задач.

Вот смотри, как это выглядит в коде:

Task firstTask = Task.Run(() => DoWork());
Task continuationTask = firstTask.ContinueWith(previousTask =>
{
    // Эта хрень запустится автоматом, когда первая задача отстреляется.
    Console.WriteLine($"Первая задача завершена со статусом: {previousTask.Status}");
});

А зачем это вообще надо? Ну, чтобы не тормозить поток, тупо ожидая, пока первая задача упрется рогом в асфальт. Вместо task.Wait() или task.Result (которые могут заблокировать всё к хуям), ты просто ставишь в очередь следующее действие, которое само запустится, когда придёт время.

Самое сольное — это флаги TaskContinuationOptions. С ними можно выебать мозг системе и указать точные условия: «Запускай продолжение только если всё было хорошо», или «только если всё пошло по пизде», или «только если операцию отменили».

Task.Run(() => ParseData())
    .ContinueWith(prevTask =>
    {
        // Выполнится ТОЛЬКО если первая задача завершилась успешно.
        ProcessResult(prevTask.Result);
    }, TaskContinuationOptions.OnlyOnRanToCompletion)
    .ContinueWith(prevTask =>
    {
        // Выполнится ТОЛЬКО если первую задачу отменили.
        LogCancellation();
    }, TaskContinuationOptions.OnlyOnCanceled)
    .ContinueWith(prevTask =>
    {
        // Выполнится ТОЛЬКО если в первой задаче вылетело исключение.
        // Всё дерьмо будет лежать в prevTask.Exception.
        LogError(prevTask.Exception?.InnerException);
    }, TaskContinuationOptions.OnlyOnFaulted);

Но есть важные моменты, блядь:

  1. С исключениями — они не похоронят твоё продолжение. Они аккуратно запаковываются в свойство Exception задачи-продолжения (в этот ёбаный AggregateException).
  2. Можно выбрать, где запускать — например, вернуться в UI-поток через TaskScheduler.FromCurrentSynchronizationContext(), чтобы обновить интерфейс.
  3. А теперь главное: в 2024 году так уже почти не пишут. Есть же async/await!

    Вот как это выглядело раньше, в каменном веке:

    Task.Factory.StartNew(() => "http://example.com/data.json")
        .ContinueWith(prev => DownloadStringAsync(prev.Result))
        .Unwrap() // Распаковываем вложенную задачу
        .ContinueWith(prev => JsonConvert.DeserializeObject<Data>(prev.Result));

    А вот как это делают нормальные люди сейчас:

    public async Task<Data> GetDataAsync()
    {
        string url = await Task.Run(() => "http://example.com/data.json");
        string json = await DownloadStringAsync(url);
        return JsonConvert.DeserializeObject<Data>(json);
    }

    Честно, второй вариант читается в тысячу раз проще. Не надо париться с этими ContinueWith и Unwrap.

Так когда же ContinueWith ещё может пригодиться?

  • Когда ты на лету собираешь цепочки задач, как лего.
  • Для каких-то низкоуровневых оптимизаций, вроде ExecuteSynchronously, чтобы выполнить продолжение в том же потоке.
  • В каком-нибудь древнем или специфичном библиотечном коде, где async/await нельзя использовать по каким-то идиотским причинам.

Но если ты не пишешь какую-то хардкорную инфраструктуру — просто юзай await. Не усложняй себе жизнь, ёпта.