是CancellationTokenSource.CancelAfter()漏水?CancellationTokenSource、CancelAfter

2023-09-05 00:53:18 作者:演绎一场只属于你我的戏

促使异步针对包的发布我使用 ILSpy 来看看什么的基于任务的异步模式(TAP)的扩展方法是只要有(其中一些我已经实施了我自己在VS2010使用)。我偶然发现的 .CancelAfter(时间跨​​度) 方法 CancellationTokenSource (这是作为一个扩展方法在异步定位包.NET 4.0,但在.NET 4.5)的实例方法,并认为这可能是实现一个超时各种操作本身并不具有超时一个很好的方法,但支持取消。

The release of the Async Targeting Pack prompted me to use ILSpy to have a look at what Task-based Asynchronous Pattern (TAP) extension methods were provided there (some of which I have already implemented on my own for use in VS2010). I stumbled upon the .CancelAfter(TimeSpan) method for CancellationTokenSource (which is as an extension method in the Async Targeting Pack for .NET 4.0 but is an instance method in .NET 4.5) and thought it could be a nice way to implement a timeout for various operations that don't natively have a timeout, but do support cancellation.

但看在异步执行针对包,看来,如果相关的工作完成或被取消,定时器继续运行。

But looking at the implementation in the Async Targeting Pack, it seems that if the associated Task completes or is canceled, the timer keeps on running.

/// <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次,这是所有成功后的几毫秒完成。后300秒时每秒100个电话30000运行定时器将所有这些行动积累,即使任务圆满完成很久以前。他们将所有最终经过和运行上面的委托,这可能会引发的ObjectDisposedException 等。

Let's say I use this method to provide a timeout for a frequently used TAP-based operation, and wrap it with a .CancelAfter(). Now let's say the user provides a timeout value of 5 minutes (300 seconds), and calls this operation 100 times a second, which all complete successfully after a few milliseconds. After 300 seconds of 100 calls per second, 30,000 running timers would have accumulated from all those operations, even though the tasks completed successfully long ago. They would all eventually elapse and run the above delegate, which will probably throw an ObjectDisposedException, etc.

是一个漏水的,不可扩展的行为不是这个有点?当我实现了一个暂停,我用任务/ TaskEx.Delay(时间跨度,的CancellationToken)当关联的任务已经结束了,我取消 .Delay ()使计时器将被停止和处置(这是一个IDisposable接口毕竟,它确实包含非托管资源)。这是清理过于热心?是具有定时器同时运行(也可能抛出捕获的异常数万更高版本)真的无关紧要的性能对于一般的应用程序成千上万的费用是多少?是的开销和泄漏 .CancelAfter()几乎都是微不足道的相比,正在进行的实际工作,一般应被忽略?

Isn't this somewhat of a leaky, non-scalable behavior? When I implemented a timeout, I used Task/TaskEx.Delay(TimeSpan, CancellationToken) and when the associated task has ended, I cancel the .Delay() so that the timer would be stopped and disposed (it is an IDisposable after all, and it does contain unmanaged resources). Is this cleanup overly zealous? Is the cost of having tens of thousands of timers running simultaneously (and possibly throwing tens of thousands of caught exceptions later) really inconsequential to performance for the average application? Is the overhead and leakiness of .CancelAfter() almost always minuscule compared to the actual work being done, and should generally be disregarded?

推荐答案

刚刚尝试它,它推到极限,看看会发生什么。我不能获得工作订走了90 MB与千万计时器。 System.Threading.Timer是很便宜的。

Just try it, push it to the limits and see what happens. I can't get working set to go over 90 MB with ten million timers. System.Threading.Timer is very cheap.

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