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

«Что такое метод ContinueWith у Task?» — вопрос из категории Многопоточность, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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.