Thread-safety of System.Timers.Timer vs System.Threading.Timer

25,345

Solution 1

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.

Solution 2

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.

Share:
25,345
Pragmateek
Author by

Pragmateek

My blog: Pragmateek I started programming at the age of 2, built my first computer at 3, invented C++ metaprogramming at 5, graduated from MIT at 9, got my third PHD from Harvard at 11 1/2, run my first startup at 12, declined Fields Medal at 13. My favorite programming language is Malbolge. In my spare time I enjoy playing hydrocrystalophone and theremin, and fixing bugs in the Linux kernel. I'm the man behind the "Jon Skeet" bot. And I'm currently cured for serious mythomania.

Updated on July 28, 2022

Comments

  • Pragmateek
    Pragmateek almost 2 years

    In this article: http://msdn.microsoft.com/en-us/magazine/cc164015.aspx the author states that System.Threading.Timer is not thread-safe.

    Since then this has been repeated on blogs, in Richter's book "CLR via C#", on SO, but this is never justified.

    Moreover the MSDN documentation assures "This type is thread safe."

    1) Who tells the truth?

    2) If this is the original article what makes System.Threading.Timer not thread-safe and how its wrapper System.Timers.Timer achieves more thread-safety?

    Thanks