Потокобезопасность System.Timers.Timer против System.Threading.Timer
В этой статье: http://msdn.microsoft.com/en-us/magazine/cc164015.aspx автор заявляет, что System.Threading.Timer не является потокобезопасным.
С тех пор это повторялось в блогах, в книге Рихтера "CLR via C#", на SO, но это никогда не оправдывалось.
Более того, документация MSDN заверяет: "Этот тип является многопоточным"
1) Кто говорит правду?
2) Если это оригинальная статья, что делает System.Threading.Timer не поточно-ориентированным и как его оболочка System.Timers.Timer обеспечивает большую безопасность потоков?
Спасибо
1 ответ
Нет, это не так. Классы асинхронного таймера.NET идеально ориентированы на многопоточность. Проблема с безопасностью потока состоит в том, что это не транзитивное свойство, оно не делает другой код, который также выполняется, потокобезопасным. Код, который вы написали, а не программист.NET Framework.
Это та же самая проблема с очень распространенным предположением, что код пользовательского интерфейса Windows принципиально не поддерживает потоки. Это не так, код внутри Windows идеально ориентирован на многопоточность. Проблема в том, что весь код, который выполняется, не является частью Windows и не написан программистом Microsoft. Там всегда много этого кода, вызванного вызовом SendMessage(). Который запускает пользовательский код, который написал программист. Или код, который он не писал, как хук, установленный какой-то утилитой. Код, который предполагает, что программа не усложняет, а просто выполняет обработчики сообщений в одном потоке. Обычно он это делает, не делая этого, он доставляет ему много хлопот.
Та же проблема с событием System.Timers.Timer.Elapsed и обратным вызовом System.Threading.Timer. Программисты делают много ошибок при написании этого кода. Он выполняется полностью асинхронно в произвольном потоке потоков, касание любой общей переменной действительно требует блокировки для защиты состояния. Очень легко пропустить. И что еще хуже, намного хуже, очень легко получить кучу неприятностей, когда код снова запустится до того, как предыдущий вызов прекратился. Срабатывает, когда интервал таймера слишком мал или машина загружена слишком сильно. Теперь есть два потока, выполняющих один и тот же код, который редко заканчивается хорошо.
Потоки трудно, новости в одиннадцать.
В System.Timers.Timer
класс не является потокобезопасным. Вот как это можно доказать. ОдинTimer
экземпляр создан, и его свойство Enabled
бесконечно переключается двумя разными потоками, работающими параллельно. Если класс является потокобезопасным, его внутреннее состояние не будет повреждено. Давайте посмотрим...
var timer = new System.Timers.Timer();
var tasks = Enumerable.Range(1, 2).Select(x => Task.Run(() =>
{
while (true)
{
timer.Enabled = true;
timer.Enabled = false;
}
})).ToArray();
Task.WhenAny(tasks).Unwrap().GetAwaiter().GetResult();
Эта программа не работает слишком долго. Исключение выдается почти сразу. Это либоNullReferenceException
или ObjectDisposedException
:
System.NullReferenceException: Object reference not set to an instance of an object.
at System.Timers.Timer.UpdateTimer()
at System.Timers.Timer.set_Enabled(Boolean value)
at Program.<>c__DisplayClass1_0.<Main>b__1()
at System.Threading.Tasks.Task`1.InnerInvoke()
at System.Threading.Tasks.Task.<>c.<.cctor>b__274_0(Object obj)
at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location where exception was thrown ---
at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
--- End of stack trace from previous location where exception was thrown ---
at Program.Main(String[] args)
Press any key to continue . . .
System.ObjectDisposedException: Cannot access a disposed object.
at System.Threading.TimerQueueTimer.Change(UInt32 dueTime, UInt32 period)
at System.Threading.Timer.Change(Int32 dueTime, Int32 period)
at System.Timers.Timer.UpdateTimer()
at System.Timers.Timer.set_Enabled(Boolean value)
at Program.<>c__DisplayClass1_0.<Main>b__1()
at System.Threading.Tasks.Task`1.InnerInvoke()
at System.Threading.Tasks.Task.<>c.<.cctor>b__274_0(Object obj)
at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location where exception was thrown ---
at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread)
--- End of stack trace from previous location where exception was thrown ---
at Program.Main(String[] args)
Press any key to continue . . .
Причина, по которой это происходит, становится очевидной после изучения исходного кода класса. При изменении внутренних полей класса синхронизации нет. Итак, синхронизируя вручную доступ кTimer
instance является обязательным, если этот экземпляр изменяется несколькими параллельными потоками. Например, приведенная ниже программа работает вечно, не вызывая никаких исключений.
var locker = new object();
var timer = new System.Timers.Timer();
var tasks = Enumerable.Range(1, 2).Select(x => Task.Run(() =>
{
while (true)
{
lock (locker) timer.Enabled = true;
lock (locker) timer.Enabled = false;
}
})).ToArray();
Task.WhenAny(tasks).Unwrap().GetAwaiter().GetResult();
Взяв во внимание System.Threading.Timer
класс, у него нет свойств, и его единственный метод Change
может вызываться несколькими потоками параллельно без каких-либо исключений. Его исходный код указывает, что он потокобезопасен, посколькуlock
используется внутри.