双线程生产者/消费者模式下最高性能线程安全实现方案咨询
场景与代码示例
我开发了一款采用生产者/消费者模式处理事件的应用(共2个线程):主线程(UI线程)原子写入引用类型字段,工作线程原子读取该字段。Demo代码如下:
public class MyApp { private Thread WorkerThread; // Thread which processes the events. private EventArgs QueuedEvent; // The latest event registered by the app. private EventArgs ProcessedEvent; // The latest event processed by the worker. public static void Main(string[] args) { // Create and start the thread. WorkerThread = new(new ThreadStart(DoWork)); WorkerThread.Start(); // Attach event [...] } // Event which is fired from the main thread. // The timing of this event is unpredictable; // it could be super fast, never fire, or anything in between. private static void OnEvent(object sender, EventArgs e) { // Set this as the latest event. // It is OK if the previous event is LOST. QueuedEvent = e; } // Main loop for consumer thread. private void DoWork() { while (true) { // Copy QueuedEvent to local variable so it's not overwritten. var argsToWorkOn = QueuedEvent; // For performance, don't do work if the args haven't changed. if (argsToWorkOn != ProcessedEvent) { // Do expensive work with argsToWorkOn. [...] // Remember that we already did work with these args. ProcessedEvent = argsToWorkOn; } } } }
核心诉求与选项
核心诉求为两个线程的性能优先级最高,绝对不能互相阻塞。在此前提下,客观上最具性能的线程安全实现方式是什么?是否属于以下选项:
- 无需处理? 显然不是(除非我理解有误)。
- 使用volatile? 这是我最初的方案,但经数小时调研后,无法确定其在现代架构下的适用性。
- 使用lock? 可能可行,但主线程可能被工作线程的读操作阻塞,影响性能。
- 使用Interlocked? 我认为适用性不强,但可能判断有误。
- 使用ReaderWriterLockSlim? 尚未深入了解,仅知晓其存在。
- 其他方案?
方案分析与结论
最优方案:使用volatile修饰符
给QueuedEvent和ProcessedEvent字段添加volatile修饰符,是完全符合现代CPU架构的无阻塞高性能方案,完美匹配你的核心诉求。
各选项逐一分析
无需处理?
确实不可行。没有同步机制时,工作线程可能因CPU缓存一致性问题,永远看不到主线程写入的QueuedEvent新值;或因指令重排导致逻辑错误,最终出现事件丢失、重复处理等问题。使用volatile?
完全适用。volatile在.NET中的作用是:- 禁止编译器和CPU对该字段的读写指令进行重排序;
- 确保每次读取直接从内存(而非CPU缓存)获取最新值,每次写入立即刷新到内存,保证线程间的可见性。
你的场景中,主线程仅做原子写入,工作线程仅做原子读取,volatile刚好满足需求,且完全无阻塞,性能开销可忽略。
使用lock?
完全没必要,且会引入不必要的阻塞。lock是互斥同步机制,主线程写入时会阻塞工作线程的读操作,反之亦然,直接违背“绝对不能互相阻塞”的核心诉求,性能表现远不如volatile。使用Interlocked?
适用性不强。Interlocked主要用于原子性的加减、交换等复杂操作,你的场景仅需简单的引用赋值(本身已是原子操作),需要的是可见性而非复杂原子操作。用Interlocked.Exchange虽能实现,但比volatile多了不必要的操作,性能更差。使用ReaderWriterLockSlim?
属于重量级同步机制,虽支持多读单写,但依然有锁开销。你的场景是单生产者单消费者,完全不需要这么复杂的结构,性能远不如volatile。其他方案?
比如Memory<T>或Unsafe类的底层内存操作,复杂度高且无必要,volatile已是最简洁高效的方案。
修改后的代码示例
public class MyApp { private Thread WorkerThread; // Thread which processes the events. private volatile EventArgs QueuedEvent; // The latest event registered by the app. private volatile EventArgs ProcessedEvent; // The latest event processed by the worker. public static void Main(string[] args) { // Create and start the thread. WorkerThread = new(new ThreadStart(DoWork)); WorkerThread.Start(); // Attach event [...] } // Event which is fired from the main thread. // The timing of this event is unpredictable; // it could be super fast, never fire, or anything in between. private static void OnEvent(object sender, EventArgs e) { // Set this as the latest event. // It is OK if the previous event is LOST. QueuedEvent = e; } // Main loop for consumer thread. private void DoWork() { while (true) { // Copy QueuedEvent to local variable so it's not overwritten. var argsToWorkOn = QueuedEvent; // For performance, don't do work if the args haven't changed. if (argsToWorkOn != ProcessedEvent) { // Do expensive work with argsToWorkOn. [...] // Remember that we already did work with these args. ProcessedEvent = argsToWorkOn; } } } }
内容的提问来源于stack exchange,提问作者Carl Schmidt

