场景是这样,假设有一台设备会触发类型为Alarm的告警信号,并把信号添加到一个Queue结构中,每隔一段时间这个Queue会被遍历检查,其中的每个Alarm都会调用一个相应的处理方法。问题在于,检查机制是基于多线程的,有潜在的并发可能,当某个Alarm被添加的同时刚好又在遍历Queue,就会抛出异常说Queue发生改变。产生问题的代码如下:
public class AlarmQueueManager
{
public ConcurrentQueue<Alarm> alarmQueue = new ConcurrentQueue<Alarm>();
System.Timers.Timer timer; public AlarmQueueManager()
{
timer = new System.Timers.Timer();
timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);
timer.Enabled = true;
} void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
DeQueueAlarm();
} private void DeQueueAlarm()
{
try
{
foreach (Alarm alarm in alarmQueue)
{
SendAlarm(alarm);
alarmQueue.TryDequeue();
//having some trouble here with TryDequeue..
}
}
catch
{
}
}
}
那么如何让DeQueueAlarm保证线程安全呢?
为了简化描述,用以下两种写法来对比介绍。
// 写法一
private void DeQueueAlarm()
{
Alarm alarm;
while (alarmQueue.TryDequeue(out alarm))
SendAlarm(alarm);
} // 写法二
private void DeQueueAlarm()
{
foreach (Alarm alarm in alarmQueue)
SendAlarm(alarm);
}
参考MSDN的说法:ConcurrentQueue<T>.GetEnumerator
。
The enumeration represents a moment-in-time snapshot of the contents of the queue. It does not reflect any updates to the collection after
GetEnumerator
was called. The enumerator is safe to use concurrently with reads from and writes to the queue.
所以两种写法在多线程并发访问的差别就是,使用TryQueue会确保每个Alarm只会被处理一次,但哪个线程处理哪个Alarm是不确定的。使用foreach循环会确保参与争用的所有线程都同等无差别地访问Queue的全部Alarm,就像Queue被重复处理了n遍。因此,如果想要严格控制每个Alarm只会被处理一次,用完就移除的话,那就使用第一种写法。