是CancellationTokenSource.CancelAfter()是否泄漏?

时间:2012-05-23 07:53:08

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

Async Targeting Pack的发布促使我使用ILSpy来查看那里提供的Task-based Asynchronous Pattern (TAP)扩展方法(其中一些已经我自己实现了使用在VS2010)。我偶然发现了CancellationTokenSource的{​​{3}}方法(这是.NET 4.0的Async Targeting Pack中的扩展方法,但它是.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()的开销和泄漏几乎总是微不足道的,通常应该被忽视吗?

1 个答案:

答案 0 :(得分:7)

试试吧,把它推到极限,看看会发生什么。我不能让工作集超过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);
    }
}