交换缓冲区对并发访问的影响



考虑一个有两个线程的应用程序,生产者消费者

两个线程的运行频率大致相同,在一秒钟内多次运行。

两个线程都访问相同的内存区域,其中生产者写入内存,消费者读取当前数据块并对其进行处理,而不会使数据无效。

一个经典的方法是这样的:

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 );
}

现在,我的问题:

  1. 如第二个示例所示,锁定是否安全?还是引用的更改会带来问题?
  2. 第二个示例中的代码执行速度会比第一个示例中的代码快吗?
  3. 有没有更好的方法来有效地解决上述问题?
  4. 有没有办法在没有锁的情况下解决这个问题?(即使我认为这是不可能的)

注意:这不是经典的生产者/消费者问题:消费者可以在生产者再次写入之前多次读取这些值 - 旧数据在生产者写入新数据之前保持有效。

如第二个示例所示,锁定是否安全?还是引用的更改会带来问题?

据我所知,因为引用赋值是原子的,这可能是安全的,但并不理想。由于 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 优化。锁定很可能适合您的情况,并且您(以及阅读您的代码的其他人)将更容易推理和维护。

最新更新