Зачем вызывать Dispose() для Task, если память освобождает сборщик мусора?

«Зачем вызывать Dispose() для Task, если память освобождает сборщик мусора?» — вопрос из категории Управление памятью, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Вызов Dispose() для Task не связан напрямую с освобождением управляемой памяти — этим действительно занимается сборщик мусора (GC). Основная причина — освобождение неуправляемых ресурсов, которые могла захватить задача в процессе своей работы.

Когда это может быть нужно?

Task может инкапсулировать объект IAsyncStateMachine и другие внутренние структуры, которые, в свою очередь, могут удерживать дескрипторы ожидания (WaitHandle), токены отмены или ресурсы ввода-вывода. Хотя эти ресурсы в конечном итоге будут освобождены финализатором, вызов Dispose() обеспечивает их детерминированную очистку.

Практический сценарий:

public async Task ProcessFileAsync(string path, CancellationToken cancellationToken)
{
    // Для асинхронной работы с файлом может быть создан внутренний WaitHandle.
    using (var stream = new FileStream(path, FileMode.Open))
    using (var reader = new StreamReader(stream))
    {
        return await reader.ReadToEndAsync();
    }
    // После завершения задачи, связанные с асинхронным I/O ресурсы
    // могут оставаться в памяти до сборки мусора.
}

// Если задача была создана, но отменена до завершения, вызов Dispose() может быть оправдан.
var cts = new CancellationTokenSource();
var task = ProcessFileAsync("large.txt", cts.Token);
cts.Cancel();

try { await task; }
catch (TaskCanceledException) { }
finally
{
    // Явное освобождение внутренних ресурсов задачи.
    // В ПОДАВЛЯЮЩЕМ БОЛЬШИНСТВЕ СЛУЧАЕВ ЭТОГО НЕ ТРЕБУЕТСЯ.
    // Делайте это, только если точно знаете, что задача удерживает дорогие ресурсы
    // и вы столкнулись с проблемами (утечками дескрипторов).
    task.Dispose();
}

Общее правило: Для задач, созданных с помощью Task.Run, Task.Factory.StartNew или async методов, явный вызов Dispose() обычно не требуется. Фреймворк эффективно управляет их жизненным циклом. Используйте Dispose() только в особых случаях, документально подтвержденных (например, при работе с Task<TResult> в связке с IValueTaskSource).