C#多线程:主线程安全等待子线程进入WaitOne阻塞的方案
C#轮询线程安全同步方案
核心竞态根因:你之前考虑的双ManualResetEvent方案失效,本质是子线程通知主线程"已到等待点"、子线程实际执行WaitOne()进入阻塞这两个操作没有原子性保证,线程调度可以在两个操作之间任意切换,导致状态判断错位。
下面的方案无死锁、无竞态,不会强制中断子线程当前轮询逻辑,不需要复杂的跨线程信号嵌套:
具体实现步骤
1. 给设备类新增同步辅助成员
不需要新增额外的重置事件,只需要加一个锁对象和状态标记即可:
public class SpeedCounterDevice { // 原有的轮询控制事件 public ManualResetEvent resetevent = new ManualResetEvent(false); // 锁对象:仅用来保护线程状态的读写 private readonly object _pollStateLock = new object(); // 轮询子线程状态 private enum PollThreadStatus { Working, // 正在执行轮询业务(网络收发、探活等) Waiting // 已阻塞在WaitOne,不会触碰套接字 } private PollThreadStatus _pollStatus = PollThreadStatus.Working; // 其他原有成员(套接字、业务方法等) }
2. 修改子线程轮询逻辑
把状态更新、通知主线程的逻辑放在锁块内,WaitOne()必须放在锁外执行,避免死锁:
private static void doPollThread(object thisObject) { SpeedCounterDevice sc = (SpeedCounterDevice)thisObject; while (true) { // 进入等待前,原子更新状态并通知主线程 lock (sc._pollStateLock) { sc._pollStatus = PollThreadStatus.Waiting; Monitor.PulseAll(sc._pollStateLock); } // 阻塞等待轮询信号,此时不持有任何锁 sc.resetevent.WaitOne(); // 被唤醒后先更新状态为工作中,再执行业务 lock (sc._pollStateLock) { sc._pollStatus = PollThreadStatus.Working; } // 原有轮询业务逻辑:网络探活、数据读取等 // do some work .... Thread.Sleep(400); } }
3. 主线程安全等待逻辑
主线程需要断开/重连套接字前,按下面的逻辑同步即可,100%保证子线程已经进入等待状态,不会碰套接字:
// 第一步:先重置轮询信号,阻止子线程下一轮继续执行 sc.resetevent.Reset(); // 第二步:阻塞等待子线程真正进入等待状态 lock (sc._pollStateLock) { // 必须用while循环判断,规避Monitor虚假唤醒问题 while (sc._pollStatus != PollThreadStatus.Waiting) { Monitor.Wait(sc._pollStateLock); } } // 代码走到这里时,子线程已经完全阻塞在WaitOne()位置 // 可以安全执行套接字断开、重连、参数修改等任何操作,不会有并发冲突 // 操作完成需要恢复轮询时,直接调用 sc.resetevent.Set() 即可
方案说明
- 无竞态:子线程更新状态、发送通知的操作在锁内原子完成,主线程必须拿到同一把锁才能检查状态,不存在操作间隙。子线程释放锁后第一时间执行
WaitOne(),等主线程拿到锁确认状态时,子线程必然已经进入阻塞。 - 无死锁:所有等待资源的操作(
WaitOne()、Monitor.Wait())都不会在持有锁的状态下执行,不会出现锁交叉等待。 - 不打断当前轮询:只有子线程完整执行完当前轮询的所有业务逻辑、跑完
Thread.Sleep(400)之后,才会更新状态通知主线程,完全符合不强制中断当前轮询的要求。 - 性能开销极低:锁粒度只覆盖状态变量的读写,不会阻塞正常的轮询业务执行。
内容的提问来源于stack exchange,提问作者Funky
相关产品推荐
相关产品推荐

