C#多线程错误:预期单线程执行的循环为何出现多线程并发?
并发控制逻辑缺陷说明
现有代码的并发控制存在多个层级的问题,其中直接导致多线程同时进入foreach循环的根因如下:
- finally块触发范围错误,导致占用标志被提前返回的线程误重置
代码将_isCurrentlyUpdatingValues = false的重置逻辑放在了整个方法的finally块中,只要进入try范围的代码,无论后续是正常执行完所有逻辑,还是在lock块中检测到标志位为true直接return,都会强制执行finally块的代码。这意味着没有拿到执行权、本来应该直接退出的线程,也会错误地把其他正在执行更新逻辑的线程设置的占用标志改成false,直接给后续线程放行。
多线程闯入的典型复现时序(单次foreach执行耗时超过定时器10秒间隔时必现):
- 首次触发的线程1进入lock,检测到
_isCurrentlyUpdatingValues为false,将其设为true后释放锁,进入foreach执行逻辑- 10秒后第二次触发的线程2进入lock,检测到标志位为true,执行return退出try块,此时会强制执行finally块,把
_isCurrentlyUpdatingValues错误重置为false,但线程1仍在foreach中运行- 后续调度的线程3进入lock时,检测到标志位已经被误改为false,就会再次将标志位设为true,释放锁后也进入foreach执行
- 上述流程会反复触发,每次有线程检测到占用提前返回,就会错误重置标志位放行新线程,最终出现3个及以上线程同时在foreach内的现象
除了核心根因外,代码还存在两个并发设计缺陷:
- 锁的保护范围与设计预期不符
锁仅覆盖了标志位的检查、赋值操作,真正需要互斥执行的foreach临界区完全在锁保护范围外;且finally中重置标志位的操作也没有被锁覆盖,多线程场景下即使没有finally误触发的问题,也可能因为指令重排、CPU缓存可见性问题,导致不同线程读取到的标志位值不一致,出现并发闯入。 - 标志位修改没有权限控制
逻辑没有区分“成功获取到执行权的线程”和“检测到占用直接退出的线程”,所有进入方法的线程都可以修改标志位的值,本身就不符合互斥锁的设计原则。
最小修复实现
修复核心是调整try/finally的范围,保证只有成功拿到执行权的线程,才能在执行完临界区逻辑后重置标志位,参考代码如下:
object _objLock = new object(); bool _isCurrentlyUpdatingValues = false; public void UpdateTestReadValuesTimerCallback(object state) { // 仅做执行权抢占,不持有锁执行长耗时逻辑 lock (_objLock) { if (_isCurrentlyUpdatingValues) { return; } _isCurrentlyUpdatingValues = true; } // 只有抢占到执行权的线程才能进入该try块 try { foreach (var prop in CommConfigDnp3.Dnp3Properties) { if (prop.ReadOrWrite == ReadOrWrite.Read) { if (prop.PropertyType == PropertyType.Bool) { bool? value = CommConfigDnp3.ReadBinary(prop.PropertyName); prop.LastValue = value; } else { double? value = CommConfigDnp3.ReadDouble(prop.PropertyName); prop.LastValue = value; } } } } finally { // 执行完临界区后,加锁重置标志位 lock (_objLock) { _isCurrentlyUpdatingValues = false; } } }
内容的提问来源于stack exchange,提问作者Eric Ouellet
相关产品推荐
相关产品推荐

