C#多线程访问静态共享变量延迟问题及实现合理性咨询
我在多线程间使用静态变量进行数据访问,但获取变量值耗时过长。场景说明如下:
我有一个静态类Results.cs,用于存储两个Process.cs实例的结果变量,代码如下:
public static int ResultsStation0 { get; set; } public static int ResultsStation1 { get; set; }
两个Process实例的StationResult方法会同时被调用,ResultsStation0/ResultsStation1初始值为-1。由于两个结果不会同时生成,该方法会先设置当前线程对应的结果,然后通过死循环等待两个结果都就绪:
void StationResult(){ Stopwatch sw = new Stopwatch(); sw.Restart(); switch (stationIndex) //Set the result of the station thread { case 0: Results.ResultsStation0 = 1; break; case 1: Results.ResultsStation1 = 1; break; } //Waits to get the results of both threads while (true) { if (Results.ResultsStation0 != -1 && Results.ResultsStation1 != -1) { break; } } Trace_Info("GOT RESULTS " + stationIndex + "Time: " + sw.ElapsedMilliseconds.ToString() + "ms"); if (Results.ResultsStation0 == 1 && Results.ResultsStation1 == 1) { //set OK if both results are OK Device.profinet.WritePorts(new Enum[] { NOK, OK }, new int[] { 0, 1 }); } }
功能可以正常运行,但等待线程的Stopwatch耗时远超预期:偶尔为1ms,但多数时候高达80ms。我的疑问是:既然共享同一内存,为何耗时如此久?当前的线程间变量访问方式是否正确?
核心问题原因
内存可见性缺失
默认的静态自动属性没有内存屏障,编译器或CPU会将变量值缓存到当前核心的本地缓存中,同时可能对读写指令重排。等待线程的死循环可能一直读取本地缓存里的旧值,直到缓存同步完成,这直接导致了延迟。忙等待的资源浪费
当前的死循环属于忙等待,会持续占用CPU资源。线程调度器可能会因为该线程占用过多资源而推迟其执行,同时也会影响其他线程的运行效率,间接拉长等待时间。
正确的线程间同步方式
方案1:用volatile保障内存可见性
给静态变量添加volatile关键字,禁止编译器和CPU对变量读写操作重排,强制每次读写都直接访问主内存:
// 注意:volatile不能搭配自动属性,需改为字段 public static volatile int ResultsStation0 = -1; public static volatile int ResultsStation1 = -1;
该方案解决了缓存一致性问题,但仍存在忙等待的CPU占用问题,适合低延迟场景的临时优化。
方案2:用CountdownEvent实现高效同步
这是更优的方案,线程等待时会进入休眠状态,不占用CPU资源:
// 在Results类中定义同步对象 public static readonly CountdownEvent ResultsReady = new CountdownEvent(2); // 修改StationResult方法 void StationResult(){ Stopwatch sw = new Stopwatch(); sw.Restart(); // 设置当前线程的结果 switch (stationIndex) { case 0: Results.ResultsStation0 = 1; break; case 1: Results.ResultsStation1 = 1; break; } // 通知同步对象:当前线程已完成结果写入 Results.ResultsReady.Signal(); // 等待所有线程完成 Results.ResultsReady.Wait(); Trace_Info("GOT RESULTS " + stationIndex + " Time: " + sw.ElapsedMilliseconds.ToString() + "ms"); if (Results.ResultsStation0 == 1 && Results.ResultsStation1 == 1) { Device.profinet.WritePorts(new Enum[] { NOK, OK }, new int[] { 0, 1 }); } }
CountdownEvent会在收到指定次数的信号(这里是2次)后,自动释放所有等待的线程,既保证了内存可见性,又彻底避免了忙等待的资源浪费。
方案3:基于Task并行模型(.NET 4.0+)
如果代码可以适配任务并行模型,这种方式更简洁高效:
// 启动两个处理任务 var task0 = Task.Run(() => { Results.ResultsStation0 = 1; }); var task1 = Task.Run(() => { Results.ResultsStation1 = 1; }); // 等待两个任务全部完成 await Task.WhenAll(task0, task1); // 后续处理逻辑 if (Results.ResultsStation0 == 1 && Results.ResultsStation1 == 1) { Device.profinet.WritePorts(new Enum[] { NOK, OK }, new int[] { 0, 1 }); }
.NET任务调度器会自动管理线程,避免手动同步的复杂度,同时保证执行效率。
总结
当前的线程间变量访问方式不正确,核心问题是缺少内存可见性保障和低效的忙等待。推荐使用CountdownEvent或任务并行模型,既能保证数据即时可见,又能避免CPU资源浪费,大幅降低等待延迟。
内容的提问来源于stack exchange,提问作者Jaume

