【问题标题】:Is CancellationTokenSource.CancelAfter() leaky?CancellationTokenSource.CancelAfter() 是否泄漏?
【发布时间】:2012-05-23 07:53:07
【问题描述】:

Async Targeting Pack 的发布促使我使用ILSpy 来看看那里提供了哪些Task-based Asynchronous Pattern (TAP) 扩展方法(其中一些我已经自己实现了用于VS2010)。我偶然发现了 CancellationTokenSource 的 .CancelAfter(TimeSpan) 方法(这是 .NET 4.0 的异步目标包中的扩展方法,但在 .NET 4.5 中是实例方法),并认为这可能是实现超时的好方法适用于本身没有超时但支持取消的各种操作。

但是查看 Async Targeting Pack 中的实现,似乎如果关联的 Task 完成或被取消,则计时器继续运行。

/// <summary>Cancels the <see cref="T:System.Threading.CancellationTokenSource" /> after the specified duration.</summary>
/// <param name="source">The CancellationTokenSource.</param>
/// <param name="dueTime">The due time in milliseconds for the source to be canceled.</param>
public static void CancelAfter(this CancellationTokenSource source, int dueTime)
{
    if (source == null)
    {
        throw new NullReferenceException();
    }
    if (dueTime < -1)
    {
        throw new ArgumentOutOfRangeException("dueTime");
    }
    Timer timer = new Timer(delegate(object self)
    {
        ((IDisposable)self).Dispose();
        try
        {
            source.Cancel();
        }
        catch (ObjectDisposedException)
        {
        }
    });
    timer.Change(dueTime, -1);
}

假设我使用此方法为经常使用的基于 TAP 的操作提供超时,并用.CancelAfter() 包装它。现在假设用户提供了 5 分钟(300 秒)的超时值,并每秒调用此操作 100 次,所有操作均在几毫秒后成功完成。在每秒 100 次调用的 300 秒后,所有这些操作将累积 30,000 个运行计时器,即使这些任务很久以前就成功完成了。他们最终都会过去并运行上述委托,这可能会抛出ObjectDisposedException等。

这不是有点泄漏、不可扩展的行为吗?当我实现超时时,我使用了Task/TaskEx.Delay(TimeSpan, CancellationToken),当相关任务结束时,我取消了.Delay(),以便停止并释放计时器(毕竟它是一个IDisposable,它确实包含非托管资源)。这种清理是不是过于热心了?让数以万计的定时器同时运行(并可能在以后抛出数以万计的捕获异常)的成本对于普通应用程序的性能真的无关紧要吗?与正在完成的实际工作相比,.CancelAfter() 的开销和泄漏几乎总是微不足道的,通常应该被忽略吗?

【问题讨论】:

  • 在某种程度上,它必须泄漏,因为您可以将同一个 CTS 与多个任务一起使用(并且 CTS 不知道其任务的状态)。此外,您可以在计时器运行时将现有 CTS 与新任务一起使用。

标签: c# .net .net-4.0 task-parallel-library .net-4.5


【解决方案1】:

试试吧,把它推到极限,看看会发生什么。我无法使用 1000 万个计时器让工作集超过 90 MB。 System.Threading.Timer 非常便宜。

using System;
using System.Threading;

class Program {
    public static int CancelCount;
    static void Main(string[] args) {
        int count = 1000 * 1000 * 10;
        for (int ix = 0; ix < count; ++ix) {
            var token = new CancellationTokenSource();
            token.CancelAfter(500);
        }
        while (CancelCount < count) {
            Thread.Sleep(100);
            Console.WriteLine(CancelCount);
        }
        Console.WriteLine("done");
        Console.ReadLine();
    }
}

static class Extensions {
    public static void CancelAfter(this CancellationTokenSource source, int dueTime) {
        Timer timer = new Timer(delegate(object self) {
            Interlocked.Increment(ref Program.CancelCount);
            ((IDisposable)self).Dispose();
            try {
                source.Cancel();
            }
            catch (ObjectDisposedException) {
            }
        });
        timer.Change(dueTime, -1);
    }
}

【讨论】:

  • 计时器使用 Win32 计时器池,这些计时器本身与硬件计时器复用。它们都是苗条的回调。
  • @LuizFelipe 所以在这种情况下,在应用程序工作集之外的系统中的其他地方是否会有很大的成本,或者在现代系统中,硬件以相对便宜和有效的方式支持 1000 万个系统?
  • 是的,因为多媒体需要高精度的硬件计时器。自 2000 年代早期以来,这不是问题。我的意思是,Realtek ACL 是最常用的 IP 块,它提供了足够好的高分辨率计时器。现代 Windows 需要至少以 66Khz 的精度作为上限的硬件。 docs.microsoft.com/en-us/windows-hardware/drivers/kernel/…
  • 另外,无论你有多少虚拟定时器,成本都不会更高,因为从硬件驱动程序到内核只有几个 DPC(延迟过程调用),像软件一样思考由硬件 IRQ 引发的 IRQ,我们不叫 IRQ,因为它使用其他机制。请记住,即使是任务计划程序也需要高分辨率计时器才能正常工作。虚拟计时器像 Windows 上的异步 I/O 一样调度,非常高效。
猜你喜欢
  • 2010-11-25
  • 2018-04-28
  • 2011-01-16
  • 2012-09-13
  • 2020-07-22
  • 2012-11-11
  • 2010-10-13
  • 2019-01-28
  • 2015-02-07
相关资源
最近更新 更多