使用 CancellationToken 执行即发即弃 Task.Delay 时的内存泄漏

Memory leak when doing fire-and-forget of Task.Delay with CancellationToken

在我的应用程序中,我需要安排一些 Action 的延迟执行。就像 JavaScript 中的 setTimeout。此外,当应用程序执行结束时,我需要取消所有尚未执行的计划执行。所以我必须在没有等待的情况下调用 Task.Delay 并将 CancellationToken 传递给它。但是如果我这样做,我将面临内存泄漏:CancellationTokenSource+CallbackNode 的 none 将被处理,直到我调用 CancelCancellationTokenSourceDispose 我从中获取CancellationTokens 传递给 Task.Delay.

最小可重现示例:

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();

执行这个例子后,它留下CancellationTokenSource+CallbackNode的1000。

如果我在 await Task.Delay(1000); 之后写 cts.Cancel() 不会出现泄漏

为什么会发生这种泄漏?所有 Task 都已完成,因此不应引用 cts.Token。将传递给 Task 的后续操作处理无济于事。
此外,如果我 await 计划执行操作的任务,则不会出现泄漏。

我有点惊讶,但看起来这是故意的。当注册被取消时,CancellationTokenSource 仍然将 CancellationTokenSource+CallbackNode 的实例保留在 free-list 中以在下一次回调中重用它。这是一个固执己见的优化,可能会适得其反。

为了说明这一点,请连续 运行 尝试你的测试两次:

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()

尽管总共注册了 2000 个回调,但您仍然会看到 CancellationTokenSource+CallbackNode 的 1000 个实例。那是因为第二次迭代重用了第一次迭代中创建的节点。

对此您无能为力,我相信这是设计使然。在任何情况下,内存量应该几乎可以忽略不计(最多 x 个实例,其中 x 是同时注册的回调的数量)。