System.Timers.Timer与System.Threading.Timer的线程安全性比较

28
在这篇文章http://msdn.microsoft.com/en-us/magazine/cc164015.aspx中,作者指出System.Threading.Timer不是线程安全的
自那时以来,这一观点在博客、Richter的书《CLR via C#》和SO上被反复提及,但从未得到证实。
此外,MSDN文档保证“此类型是线程安全的”。
1)谁说的才是真相?
2)如果这是原始文章,是什么使System.Threading.Timer不是线程安全的,而其包装器System.Timers.Timer如何实现更多的线程安全性?
谢谢。

7
线程安全可以有许多不同的解释,因此每个人都是正确的。 - H H
7
过多的线程会严重破坏您电脑的状态。 - H H
3
问题在于两个页面都没有很清楚地说明它的意思。 - H H
5
请仔细阅读这个定义:“线程安全的程序是指对于线程而言是安全的程序”!这个定义几乎没有任何用处。 - Eric Lippert
2
@usr: 我没有真正的用例,只是出于好奇。此外,我在编程中学到了一个黄金法则:永远不要假定某些东西是无意义的,因为你几乎总能找到有相关用例的人。 ;) - Pragmateek
显示剩余6条评论
2个回答

48
不,这不是它的工作方式。.NET异步计时器类是完全线程安全的。线程安全的问题在于它不是一个传递属性,它不能使其他执行的代码也变得线程安全。你编写的代码有问题,不是.NET框架程序员。
这与非常普遍的假设Windows UI代码基本上不安全有相同的问题。它不是这样的,Windows内部的代码是完全线程安全的。问题在于运行的所有代码都不是Windows的一部分,并且不是由Microsoft程序员编写的。总是有大量的该代码,由SendMessage()调用触发。这将运行程序员编写的自定义代码或未经其编写的代码,例如某个实用程序安装的挂钩。这些代码假定程序不会让它变得困难,并仅在一个线程上执行消息处理程序。他通常不会这样做,不这样做会带来很多麻烦。
System.Timers.Timer.Elapsed事件和System.Threading.Timer回调面临同样的问题。程序员在编写该代码时会犯很多错误。它在任意线程池线程上完全异步运行,触及任何共享变量确实需要锁定以保护状态,这很容易忽略。更糟糕的是,在前一个调用停止运行之前,很容易让自己陷入一堆麻烦,代码再次运行。当计时器间隔太低或机器负载过重时触发。现在有两个线程运行相同的代码,这很少有好结果。
线程编程很难,11点钟的新闻。

2
感谢Hans提醒我们这个基本原则。我知道这个问题(通过艰苦的学习得到了教训;))。但是有些人(不仅仅是任何人)声称这些计时器之间的一个基本区别是线程安全性。正如与Henk讨论的那样,你说的两者都应该被认为是线程安全的,或者两者都应该被认为不是线程安全的。也许解释很简单:原始源代码是错误的(例如,因为基于早期的alpha实现),其他人只是重复了这个错误的陈述。但我想确定一下。 - Pragmateek
1
这是一个很棒的答案。@Pragmateek,我认为你应该接受这个作为回答。 - Sudhanshu Mishra
4
这个回答没有被接受的原因并不是它的质量不好,实际上它的质量非常优秀,而是因为其他来源对该主题持相反观点仍存在合理的怀疑。但是,如果我们把Hans视为权威来源(在我看来这是一个合理的假设),那么我会接受这个回答。 :) - Pragmateek
2
仅作警告:System.Timers.Timer中的Enabled属性不是线程安全的。只需查看其源代码即可了解此情况。(就像几乎所有.Net类的文档所说的那样:此类型的任何公共静态成员都是线程安全的。但是,任何实例成员都不能保证是线程安全的。) - Markus
2
“.NET异步计时器类是完全线程安全的” - 不,只有System.Threading.Timer类被记录为线程安全。System.Timers.Timer类明确记录为不保证线程安全,假设它是线程安全的是不明智的。 - Peter Duniho

11

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实例时,手动同步访问是必要的。例如,下面的程序永远不会抛出任何异常。
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

1
感谢这个非常有趣的测试。现在变得有些荒谬了,本应该是线程安全的不是,而本不应该是线程安全的却是(我试图破坏它,但是不可能)。 :) - Pragmateek
2
这是正确的答案,也与两个类的官方文档一致。 - Mo B.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接