Thread-safety of System.Timers.Timer vs System.Threading.Timer
No, that's not the way it works. The .NET asynchronous Timer classes are perfectly thread-safe. The problem with thread-safety is that it is not a transitive property, it doesn't make the other code that's executed thread-safe as well. The code that you wrote, not a .NET Framework programmer.
It is the same kind of problem with the very common assumption that Windows UI code is fundamentally thread-unsafe. It is not, the code inside Windows is perfectly thread-safe. The problem is all the code that runs that is not part of Windows and not written by a Microsoft programmer. There's always a lot of that code, triggered by a SendMessage() call. Which runs custom code that a programmer wrote. Or code he didn't write, like a hook installed by some utility. Code that assumes that the program doesn't make it difficult and just executes message handlers on one thread. He usually does, not doing that buys him a lot of trouble.
Same problem with the System.Timers.Timer.Elapsed event and the System.Threading.Timer callback. Programmers make lots of mistakes writing that code. It runs complete asynchronously on an arbitrary threadpool thread, touching any shared variable really does require locking to protect state. Very easy to overlook. And worse, much worse, very easy to get yourself into a pile of trouble when the code runs again, before the previous invocation stopped running. Triggered when the timer interval is too low or the machine is too heavily loaded. Now there are two threads running the same code, that rarely comes to a good end.
Threading is hard, news at eleven.
The System.Timers.Timer
class is not thread safe. Here is how it can be proved. A single Timer
instance is created, and its property Enabled
is toggled endlessly by two different threads that are running in parallel. If the class is thread safe, its internal state will not be corrupted. Lets see...
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();
This program is not running for too long. An exception is thrown almost immediately. It is either a NullReferenceException
or an 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 . . .
The reason this happens is quite evident, after studying the source code of the class. There is no synchronization when the internal fields of the class are changed. So synchronizing manually the access to a Timer
instance is mandatory, when this instance is mutated by multiple threads in parallel. For example the program below runs forever without throwing any exception.
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();
Regarding the System.Threading.Timer
class, it has no properties, and its single method Change
can be called by multiple threads in parallel without any exceptions thrown. Its source code indicates that it's thread safe, since a lock
is used internally.