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