我有一个应用程序,它利用生产者/消费者模式来处理事件(总共 2 个线程)。主(UI)线程自动写入引用类型字段,而我的实例化线程自动读取该字段。这是演示代码:
公开课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;
}
}
}
}
两个线程中的性能是这里的第一要务;他们不得互相阻碍。鉴于这种情况,客观上实现线程安全的最高效方法是什么?是:
反对 volatility 的论点,特别是在现代建筑方面。使用锁?
使用
volatile
volatile
就 CPU 利用率而言,这是绝对的赢家,而就您自己的时间和理智而言,这是绝对的性能输家。在混乱消失并且您对
private volatile EventArgs QueuedEvent;
private volatile EventArgs ProcessedEvent;
在实践中为您提供的保证有一个牢固的理解之前,您需要几个月的学习,尤其是对.NET运行时的源代码的研究。我说在实践中,因为可以说,根据官方文档,.NET 源代码本身是不正确的,因为它做出了文档没有提供的假设。
volatile
关键字确保代码的CPU指令不会以允许一个线程看到处于部分初始化状态的volatile
实例的方式重新排序。它还确保编译器不会发出仅读取一次
QueuedEvent
的代码,然后不再读取它,随后始终返回相同的缓存值。读取 QueuedEvent
字段的线程将看到该字段的相当新鲜的值,大约与使用 volatile QueuedEvent
或“Interlocked”时一样新鲜。也就是说,在进行了长达数月的研究之后,您很可能会得出这样的结论:当前状态下的代码是一堆大便,您应该尽快丢弃并重新开始,利用您将学到的所有知识其间已积累。 .NET 平台提供了一系列强大而高效的工具来实现生产者/消费者模式,尝试通过自己的聪明才智来超越这些工具只会导致浪费大量时间来尝试调试应用程序的不稳定行为。并且您的定制解决方案所谓的卓越性能永远不会实现任何有形/可衡量的指标。