考虑一个有两个线程的应用程序,生产者和消费者。
两个线程的运行频率大致相同,在一秒钟内多次运行。
两个线程都访问相同的内存区域,其中生产者写入内存,消费者读取当前数据块并对其进行处理,而不会使数据无效。
一个经典的方法是这样的:
int[] sharedData;
//Called frequently by thread Producer
void WriteValues(int[] data)
{
lock(sharedData)
{
Array.Copy(data, sharedData, LENGTH);
}
}
//Called frequently by thread Consumer
void WriteValues()
{
int[] data;
lock(sharedData)
{
Array.Copy(sharedData, data, LENGTH);
}
DoSomething(data);
}
如果我们假设Array.Copy
需要时间,则此代码将运行缓慢,因为生产者在复制过程中始终必须等待 Consumer,反之亦然。
解决此问题的一种方法是创建两个缓冲区,一个由使用者访问,另一个由生产者写入,并在写入完成后立即交换缓冲区。
int[] frontBuffer;
int[] backBuffer;
//Called frequently by thread Producer
void WriteValues(int[] data)
{
lock(backBuffer)
{
Array.Copy(data, backBuffer, LENGTH);
int[] temp = frontBuffer;
frontBuffer = backBuffer;
backBuffer = temp;
}
}
//Called frequently by thread Consumer
void WriteValues()
{
int[] data;
int[] currentFrontBuffer = frontBuffer;
lock(currentForntBuffer)
{
Array.Copy(currentFrontBuffer , data, LENGTH);
}
DoSomething(currentForntBuffer );
}
现在,我的问题:
- 如第二个示例所示,锁定是否安全?还是引用的更改会带来问题?
- 第二个示例中的代码执行速度会比第一个示例中的代码快吗?
- 有没有更好的方法来有效地解决上述问题?
- 有没有办法在没有锁的情况下解决这个问题?(即使我认为这是不可能的)
注意:这不是经典的生产者/消费者问题:消费者可以在生产者再次写入之前多次读取这些值 - 旧数据在生产者写入新数据之前保持有效。
如第二个示例所示,锁定是否安全?还是引用的更改会带来问题?
据我所知,因为引用赋值是原子的,这可能是安全的,但并不理想。由于 WriteValues()
方法在没有锁或内存屏障强制缓存刷新的情况下从frontBuffer
读取,因此无法保证变量会使用主内存中的新值进行更新。然后,有可能从本地寄存器或 CPU 缓存中连续读取该实例的过时缓存值。我不确定编译器/JIT 是否会根据局部变量推断缓存刷新,也许具有更具体知识的人可以谈论这个领域。
即使这些值没有过时,您也可能会遇到比您希望的更多的争用。例如。。。
- 线程 A 调用
WriteValues()
- 线程 A 在
frontBuffer
中锁定实例并开始复制。 - 线程 B 调用
WriteValues(int[])
- 线程 B 写入其数据,将当前锁定的
frontBuffer
实例移动到backBuffer
中。 - 线程 B 调用
WriteValues(int[])
- 线程 B 在锁上等待
backBuffer
,因为线程 A 仍然拥有它。
第二个示例中的代码执行速度会比第一个示例中的代码快吗?
我建议你分析一下并找出答案。X比Y快只有在Y对于你的特定需求来说太慢时才重要,而你是唯一知道这些是什么的人。
有没有更好的方法来有效地解决上述问题?
是的。如果您使用的是 .Net 4 及更高版本,则 System.Collections.Concurrent 中有一个 BlockingCollection 类型,可以很好地模拟生产者/消费者模式。如果你的阅读量一直比你写的多,或者有多个读者到很少的作家,你可能还需要考虑ReaderWriterLockSlim类。作为一般经验法则,您应该尽可能少地在锁内做事,这也将有助于缓解您的时间问题。
有没有办法在没有锁的情况下解决这个问题?(即使我认为这是不可能的)
您也许可以,但我不建议您尝试,除非您非常熟悉多线程、缓存一致性和潜在的编译器/JIT 优化。锁定很可能适合您的情况,并且您(以及阅读您的代码的其他人)将更容易推理和维护。