使用 CancellationToken 执行即发即弃 Task.Delay 时的内存泄漏
Memory leak when doing fire-and-forget of Task.Delay with CancellationToken
在我的应用程序中,我需要安排一些 Action
的延迟执行。就像 JavaScript 中的 setTimeout
。此外,当应用程序执行结束时,我需要取消所有尚未执行的计划执行。所以我必须在没有等待的情况下调用 Task.Delay
并将 CancellationToken
传递给它。但是如果我这样做,我将面临内存泄漏:CancellationTokenSource+CallbackNode
的 none 将被处理,直到我调用 Cancel
和 CancellationTokenSource
的 Dispose
我从中获取CancellationToken
s 传递给 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
是同时注册的回调的数量)。
在我的应用程序中,我需要安排一些 Action
的延迟执行。就像 JavaScript 中的 setTimeout
。此外,当应用程序执行结束时,我需要取消所有尚未执行的计划执行。所以我必须在没有等待的情况下调用 Task.Delay
并将 CancellationToken
传递给它。但是如果我这样做,我将面临内存泄漏:CancellationTokenSource+CallbackNode
的 none 将被处理,直到我调用 Cancel
和 CancellationTokenSource
的 Dispose
我从中获取CancellationToken
s 传递给 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
是同时注册的回调的数量)。