C++ 数据结构的线程安全,在哪里添加同步原语?

C++ 数据结构的线程安全,在哪里添加同步原语?,c++,c,multithreading,thread-safety,pthreads,C++,C,Multithreading,Thread Safety,Pthreads,这是一个困扰我一段时间的设计问题。这真的很简单,当您提供数据结构库时,您是在线程安全原语中构建还是只提供构造并让使用它的系统决定如何实现实际操作 一个简单的例子,一个支持生产者、消费者模型的循环缓冲区。两个方法,Get和Write,每个方法更新一个全局变量填充计数。现在,您是只提供要锁定的互斥体,让使用缓冲区的代码获取互斥体,还是在内部执行锁定并提供开箱即用的互斥 STL似乎采取从外部执行的方法,但您希望提供更细粒度的锁定,这是出于性能原因 想法?如果可以,请不要锁定 如果没有办法,你有两个选择

这是一个困扰我一段时间的设计问题。这真的很简单,当您提供数据结构库时,您是在线程安全原语中构建还是只提供构造并让使用它的系统决定如何实现实际操作

一个简单的例子,一个支持生产者、消费者模型的循环缓冲区。两个方法,Get和Write,每个方法更新一个全局变量填充计数。现在,您是只提供要锁定的互斥体,让使用缓冲区的代码获取互斥体,还是在内部执行锁定并提供开箱即用的互斥

STL似乎采取从外部执行的方法,但您希望提供更细粒度的锁定,这是出于性能原因


想法?

如果可以,请不要锁定

如果没有办法,你有两个选择:(1)内部锁定(2)外部锁定


(1) 最好的方法是内部锁定。 (2) 另一种方法是让用户处理并发性问题

无论采用哪种方式,都必须对类进行文档记录,以让用户/调用者知道它如何处理并发性

以下是来自有效Java的总结:

总之,每个类都应该清楚地记录其线程安全属性 使用措辞谨慎的散文描述或线程安全注释。这个 同步修改器在本文档中不起作用。有条件地 线程安全类必须记录哪些方法调用序列需要 外部同步,以及执行这些同步时要获取哪个锁 序列。如果编写无条件线程安全类,请考虑使用私有 锁定对象以代替同步方法。这可以防止同步 客户机和子类的干扰,并使您能够灵活地 在以后的版本中采用更复杂的并发控制方法


<这里有两个重要的问题要考虑:

  • 这些操作是单独使用还是在某些情况下可以一起使用
  • 这些操作是否也可以在单线程环境中使用
  • 第1点有一些有趣的含义。如果您在内部锁定,那么如果您只使用每个操作本身,那么您是安全的。但是,如果您可能在一个序列中使用两个或多个,请记住,每个操作的原子性并不保证整个序列的原子性,因此在任何情况下都需要外部锁定。例如:

    if(buffer not empty)
        extract from buffer
    
    尽管这两个操作本身都是原子操作,但由于明显的原因,上面的代码不是线程安全的


    第2点也是反对内部锁的一个论点:在单线程环境中,您不需要锁,因此通过获取和释放内部锁,您会产生不必要的开销。例如,这就是Java中弃用
    哈希表
    向量
    类的原因之一。

    IMHO没有明确的胜利者。董事会的任何一方都有利弊:

    将同步作为API的一部分(在模块内):

    • 确保调用者不必花太多时间考虑同步
    • 确保调用者在同步时不会出错(因为在没有内置同步结构支持的语言中进行同步可能非常容易出错)
    • 您可以进行更细粒度的锁定并优化库
    让调用者同步

    • 给调用者更多的控制权
    • 在单线程程序中,调用方不会花费时间锁定/解锁
    您可以根据具体情况做出决定:

    • 如果它是一个最有可能在多线程情况下使用的库,那么提供内置锁定
    • 如果锁的实现非常繁琐(比如线程安全队列的每个节点锁),那么将其作为库的一部分提供
    • 考虑提供两个版本的库-锁定和解锁。在C++中使用模板,提供一个与并发模式耦合的好语法,例如线程安全接口< /LI>
    • 始终如一!如果要在库中提供一组模块,请确保线程安全模块与非线程安全模块的语法一致。这是我对JavaSwing的不满,因为它们不一致。库的某些部分是线程安全的,而其他部分则不是

    希望这有帮助

    Herb Sutter和Andrei Alexandrescu建议如下:

    如果应用程序跨线程共享数据,请安全地执行以下操作:

    • 有关本地同步原语,请参阅目标平台的文档
    • 更愿意将平台的原语封装在您自己的抽象中
    • 确保在多线程程序中使用的类型是安全的
    • 确保非共享对象是独立的
    • 记录调用方在不同线程中使用该类型的同一对象所需的操作

    这篇文章讨论了线程安全设计的三种方法:内部、外部和无锁,因此您可能会发现它很有用。

    不久前我就在思考这个确切的问题。因此,我继续写了一些示例代码,以了解各种方法的优缺点。因此,与其给出一个理论上的答案,不如让我给你一些代码,来解决你在OP中提到的同样的问题,即具有多个生产者和消费者的循环缓冲区(队列)

    是的


    也许看看代码可以给你一些解释。如果需要的话,我将补充更多的观点。。但是现在,看看代码,得出明显的结论

    如果处理数据结构的方法很小,只需要一些指令,就不应该这样做
    // adding single bytes
    
    i=circ.head;
    circ.buffer[i]=chr;
    ++i;
    if (i==circ.limit) i=0;
    circ.head=i;
    
    // removing single bytes
    
    i=circ.tail;
    if (i!=circ.head)    /* there's data in the buffer */
    {
      chr=circ.buffer[i];
      ++i;
      if (i==circ.limit) i=0;
      circ.tail=i;
    }