En mi aplicación, necesito programar la ejecución de algunas Action con retraso. Es como setTimeout en JavaScript. Además, cuando finaliza la ejecución de la aplicación, necesito cancelar todas las ejecuciones programadas que aún no se han ejecutado. Así que tengo que llamar a Task.Delay sin esperar y pasarle CancellationToken . Pero si lo hago, me enfrento a una fuga de memoria: ninguno de CancellationTokenSource+CallbackNode se desechará hasta que llame a Cancel y Dispose de CancellationTokenSource desde donde tomo CancellationToken s para pasar a Task.Delay .
Ejemplo mínimo reproducible:
CancellationTokenSource cts = new CancellationTokenSource(); for (int i = 0; i < 1000; i++) { Task.Delay(500, cts.Token).ContinueWith(_ => Console.WriteLine("Scheduled action")); } await Task.Delay(1000); Console.ReadLine(); Después de ejecutar este ejemplo, deja 1000 de CancellationTokenSource+CallbackNode . 
Si escribo cts.Cancel() después de await Task.Delay(1000); la fuga no aparece 
¿Por qué ocurre esta fuga? Todas las Task se completaron, por lo que no debería haber referencias a cts.Token . Deshacerse de Task pasada a la acción de continuación no ayuda.
Además, si await una tarea que programe la ejecución de la acción, no aparece la fuga.
Me sorprendió un poco, pero parece que esto es a propósito. Cuando se cancela un registro, CancellationTokenSource aún mantiene la instancia de CancellationTokenSource+CallbackNode en una lista libre para reutilizarla para la próxima devolución de llamada. Esta es una optimización de opinión, que puede ser contraproducente en su escenario.
Para ilustrar esto, intente ejecutar su prueba dos veces seguidas:
var tasks = new Task[1000]; for (int i = 0; i < 1000; i++) { tasks[i] = Task.Delay(500, cts.Token).ContinueWith(_ => Console.WriteLine("Scheduled action")); } await Task.WhenAll(tasks); for (int i = 0; i < 1000; i++) { tasks[i] = Task.Delay(500, cts.Token).ContinueWith(_ => Console.WriteLine("Scheduled action")); } await Task.WhenAll(tasks); Console.ReadLine() Verá que todavía hay 1000 instancias de CancellationTokenSource+CallbackNode , a pesar de registrar 2000 devoluciones de llamada en total. Eso es porque la segunda iteración reutilizó los nodos creados durante la primera.
No hay mucho que puedas hacer al respecto, creo que esto es por diseño. En cualquier caso, la cantidad de memoria debería ser prácticamente insignificante (como máximo x instancias, donde x es el número de devoluciones de llamada registradas simultáneamente).