Является ли CancellationTokenSource.CancelAfter() негерметичным?
Релиз Async Targeting Pack побудил меня использовать ILSpy, чтобы взглянуть на то, какие там были предоставлены методы расширения Asynchronous Pattern (TAP) на основе задач (некоторые из которых я уже реализовал самостоятельно для использования в VS2010). Я наткнулся на .CancelAfter(TimeSpan)
метод для CancellationTokenSource
(который является методом расширения в Async Targeting Pack для.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 раз в секунду, и все они успешно завершаются через несколько миллисекунд. После 300 секунд из 100 вызовов в секунду от всех этих операций накапливалось бы 30000 запущенных таймеров, даже если задачи были успешно завершены давно. Все они в конечном итоге истекут и запустить вышеупомянутого делегата, который, вероятно, ObjectDisposedException
, так далее.
Разве это не утечка, не масштабируемое поведение? Когда я реализовал тайм-аут, я использовал Task/TaskEx.Delay(TimeSpan, CancellationToken)
и когда связанная задача закончилась, я отменяю .Delay()
так что таймер будет остановлен и утилизирован (в конце концов, это IDisposable, и он содержит неуправляемые ресурсы). Это очистка чрезмерно усердствует? Является ли стоимость одновременной работы десятков тысяч таймеров (и, возможно, последующие выбросы десятков тысяч обнаруженных исключений) действительно несущественной для производительности среднего приложения? Это накладные расходы и утечки .CancelAfter()
почти всегда крохотный по сравнению с фактической работой, и должен вообще игнорироваться?
1 ответ
Просто попробуйте, доведите до предела и посмотрите, что произойдет. Я не могу получить рабочий набор более 90 МБ с десятью миллионами таймеров. 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);
}
}