0

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

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

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

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

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需要时间,那么这段代码会运行得很慢,因为在复制过程中生产者总是必须等待消费者,反之亦然。

解决此问题的一种方法是创建两个缓冲区,一个由消费者访问,一个由生产者写入,并在写入完成后立即交换缓冲区。

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. 有没有办法在没有锁的情况下解决这个问题?(即使我认为不可能)

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

4

1 回答 1

1

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

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

于 2013-09-13T22:43:19.183 回答