假设我有一个计时器(例如System.Timers.timer),我们知道每个延迟的事件都会被放入线程池中。如果事件发生得足够快,线程池如何管理对共享变量(例如全局int计数器)的访问。经理是否在幕后使用信号灯/锁?
或者它什么都不做,只是在线程池的开始处复制共享变量,最后一个完成的线程将设置正确的变量值?
不幸的是,我无法真正测试这一点,因为在每个经过的事件之间,事件触发的顺序没有得到保证(例如,使用计数器变量是不可靠的),因为它们可能会无序触发。
感谢
您必须自己管理对共享变量的多线程访问。
在StackOverflow和谷歌上有很多答案解释了如何做到这一点,搜索"线程安全C#"。
我曾参与过许多潜在线程问题的大型项目,我编写的代码很有效。这些天我非常擅长编写线程安全代码,因为我已经犯了所有可能的错误。
如果你只是在学习编写线程安全的代码,那么很容易被大量的信息淹没。您可能会发现一些页面涵盖了8种不同类型的同步原语。你会发现关于这个话题的大量讨论,其中只有一半会有所帮助。
如果你是第一次遵循学习曲线,我建议你暂时忽略所说的噪音,而是先专注于掌握这两条规则:
规则1
如果任何两个线程写入某个共享原语(如long
、Dictionary
或List
),则在访问该共享原语时放置lock
。针对这样一种情况,即当锁定完成时,数据结构将完全更新。这是编写线程安全代码的核心:所有其他线程规则都可以从这个规则中派生出来。
示例:
// This _lock should be initialized once on program startup, and should be global.
static readonly object _dictLock = new object();
// This data structure can be accessed by multiple threads.
public static Dictionary<string, int> dict = new Dictionary<string, int>();
lock (_dictLock)
{
if (dict.ContainsKey("Hello") == false)
{
dict.Add("Hello", 42);
}
} // Lock exits: data structure is now completely 100% updated. Google "atomic access C#".
规则2
锁中尽量不要有锁。如果按错误的顺序输入锁,这可能会造成死锁。如果只锁定基元(例如dictionary
、long
、string
等),那么这应该不是问题。
准则1
如果你只是在学习,除了lock
什么都不用,看看如何使用锁。如果你只是这样做,很难出错,因为当函数退出时,锁会自动释放。稍后,你可以升级到其他类型的锁,比如读写锁。现在还不必为ConcurrentDictionary
或Interlocked.Increment
而烦恼——专注于正确掌握基本知识。
准则2
尽量少在锁里呆时间。不要在一大块代码周围加锁,要在代码中尽可能小的部分加锁,通常是dictionary
或long
。锁的速度非常快,除非它有争议,所以这种技术似乎可以很好地创建快速的线程安全代码。
95%有意义的线程问题的原因
根据我的经验,线程不安全代码的最大原因是Dictionary
。即使是ConcurrentDictionary
也不能幸免——如果访问分布在多条线路上,它需要手动锁定才能正确。如果你做对了,你将消除代码中95%有意义的线程问题。
线程池不能神奇地使共享的可变变量线程安全。它无法控制它们,甚至不知道它们的存在。
请注意,定时器滴答声可能同时发生(即使是在低频率下),并且在定时器被释放后发生。您需要执行任何必要的同步。
线程池本身是线程安全的,因为我可以成功地处理并发工作项(这是一点)。