При каких условиях уничтожается Task?
Абстрактный пример: есть _reader, который асинхронно читает данные. Есть cancellationToken, который станет Cancel, когда приложение должно завершить свою работу:
while (!cancellationToken.IsCancellationRequested)
{
var readingTask = _reader.ReadAsync();
await Task.WhenAny(readingTask, Task.Delay(Timeout.Infinite, cancellationToken));
if (readingTask.IsCompletedSuccessfully)
{
var readResult = readingTask.Result;
// Обрабатываем readResult
}
}
Предположим, что цикл выполнился раз 40. Будет ли засоряться память либо проседать производительность из-за того, что каждую итерацию цикла создается задача Task.Delay(Timeout.Infinite, cancellationToken), которая завершится только в случае, когда токен будет Cancel? Либо сборщик мусора (либо кто-то еще) будет уничтожать эти бесконечные задачи?
Ответы (2 шт):
Я потестировал вырожденный случай - вообще без cancellationToken, тут это по-моему не важно. Результат - память таки кончается, не смотря на работу сборщика мусора.
using System;
using System.Threading.Tasks;
public class Program
{
public static async void Main()
{
Console.WriteLine("Begin...");
Console.WriteLine("Memory used {0:N0}", GC.GetTotalMemory(false));
for(var i = 0; i < 100000; i++)
{
await Task.WhenAny(Task.CompletedTask, Task.Delay(100000));
if(i % 10000 == 0)
{
Console.WriteLine("Collecting");
GC.Collect();
GC.Collect();
Console.WriteLine("Memory used {0:N0}", GC.GetTotalMemory(true));
}
}
Console.WriteLine("Hello World!");
}
}
Вывод:
Begin...
Memory used 8,159,128
Collecting
Memory used 7,964,504
Collecting
Memory used 27,284,692
Fatal Error: Memory usage limit was exceeded
Ссылка, по которой можно проверить работу кода.
Как и любой объект, Task уничтожается когда на него не остаётся ссылок. Обычно ссылок на него - две:
- от потребителя объекта (того, кто ждёт);
- от того, кто выполняет работу.
Первую ссылку удерживаете вы сами (для временных объектов её "держит" только оператор await). А вот вторая ссылка живёт пока задача не окажется завершена либо отменена.
Соответственно, обычно для уничтожения Task необходимо и достаточно забыть про него потребителю и отменить работу. Но в каждом индивидуальном случае могут быть свои особенности.
Теперь о Task.Delay. Ссылка на Task.Delay удерживается активным таймером, и пропадает при отмене таймера. Таким образом, вам достаточно отменить таймер чтобы объект был собран GC:
using (var tempCts = CancellationTokenSource.CreateLinkedSource(cancellationToken)) {
await Task.WhenAny(readingTask, Task.Delay(Timeout.Infinite, tempCts.Token));
tempCts.Cancel(); // отменяет таймер и даёт собрать задачу
}
Однако, судя по Timeout.Infinite, я вижу что эта задача является "вечной". Тут можно обойтись вовсе без Task.Delay:
var tcs = new TaskCompletionSource<object>();
using (cancellationToken.Register(tcs.SetCanceled))
await Task.WhenAny(readingTask, tcs.Task);
Отмечу, что using тут обязателен, иначе ссылка на tcs будет удерживаться токеном отмены.
PS третий способ сделать то, что вы добиваетесь - вот такой:
await readingTask.ContinueWith(_ => {}, cancellationToken);
Здесь ContinueWith тоже создаёт временную задачу, ссылка на которую удерживается пока она не будет завершена или отменена (однако, в отличии от варианта с Task.Delay, она завершается сразу после завершения readingTask).