System.Timers.Timer vs System.Threading.Timer 的线程安全
本文关键字:System Timer 线程 安全 vs Timers Threading | 更新日期: 2023-09-27 18:36:44
在本文中:http://msdn.microsoft.com/en-us/magazine/cc164015.aspx 作者指出System.Threading.Timer 不是线程安全的。
从那以后,这在Richter的书"CLR via C#"中重复出现,但这从来没有被证明是合理的。
此外,MSDN 文档保证"此类型是线程安全的"。
1)谁说实话?
2)如果这是原始文章,是什么使System.Threading.Timer不是线程安全的,以及它的包装器System.Timers.Timer如何实现更多的线程安全?
谢谢
不,这不是它的工作方式。 .NET 异步计时器类是完全线程安全的。 线程安全的问题在于它不是一个可传递属性,它不会使执行的其他代码也成为线程安全的。 您编写的代码,而不是 .NET Framework 程序员。
这与非常常见的假设(即Windows UI代码从根本上是线程不安全的)是相同的问题。 事实并非如此,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
实例被多个线程并行改变时,手动同步对实例的访问是强制性的。例如,下面的程序永远运行,不会引发任何异常。
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
。