【问题标题】:Thread Safety of datatstructures, Where to add synchronization primitives?数据结构的线程安全,在哪里添加同步原语?
【发布时间】:2012-02-03 16:38:46
【问题描述】:

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

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

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

想法?

【问题讨论】:

  • 如果您将线程安全直接构建到您的数据结构中,您不仅会紧密耦合两个独立的设计概念,而且可能会降低一些性能。
  • @AJG85 好的性能参数,如果你将它构建到数据结构中,你实际上可以设计更细粒度的锁定原语。我认为性能可能会更好。
  • @shrin:我的意思是获取锁需要额外的资源。在单线程应用程序或某些类型的容器中,您最终可能会为不需要的东西付费。

标签: c++ c multithreading thread-safety pthreads


【解决方案1】:

恕我直言,这次讨论没有明确的赢家。董事会的任何一方都有利有弊:

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

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

让调用者同步

  • 为调用者提供更多控制权
  • 在单线程程序中,调用者不会花费时间锁定/解锁

您可以根据情况做出决定:

  • 如果它是最有可能在多线程情况下使用的库,则提供内置锁定
  • 如果实现锁定非常繁琐(例如线程安全队列的每个节点锁定),则将其作为库的一部分提供
  • 考虑提供 2 个版本的库 - 锁定和解锁。在 C++ 中使用模板来提供良好的语法以及并发模式,例如 ThreadSafe 接口
  • 保持一致!如果您在库中提供一组模块,请确保您的线程安全与非线程安全模块的语法一致。这是我对 Java Swing 的不满,即它们不一致。库的某些部分是线程安全的,而其他部分则不是。

希望这会有所帮助!

【讨论】:

    【解决方案2】:

    前段时间我在考虑确切的问题。所以我继续写了一些示例代码来了解各种方法的优缺点。因此,我没有给出理论上的答案,而是为您提供一些代码来解决您在 OP 中提到的相同问题,即具有多个生产者和消费者的循环缓冲区(队列)。

    Here 是。

    也许查看代码可能会给您一些澄清。如果需要,我将添加更多点。但现在,看看代码并得出明显的结论!

    【讨论】:

    • 你的代码就是一个很好的例子,你在整个方法和 While 循环中获取了一个互斥锁。现在这是我想通过内部锁定避免的确切行为。在循环队列中,您真正需要锁定的是填充计数如何更新,仅此而已
    • @shrin 现在你在说我的朋友.. 这很好!编写示例程序可以帮助我理解一些设计问题。我很高兴它以某种形式帮助了你!谢谢! :)
    • @sangeeth-saravanarj 是的,我经历了相同的过程,内部锁定存在一个问题。假设您有内部 Enqueue 等到有可用空间,如果有空则 Dequeue 等待。现在,只有外部实体知道是否有更多数据可能进入队列,因此您最终会处于队列总是忙于等待的位置
    • 是的,我同意。在这种情况下会有一些竞争条件。然而,当我用 100 个生产者和 100 个消费者运行这个程序时,队列大小只有 100 个,一夜之间,我没有遇到任何死锁/活锁。这就是多线程编程的美妙之处,它很难测试极端情况!
    【解决方案3】:

    如果可以,请不要锁定。

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

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

    无论采用哪种方式,您都必须记录您的类,让用户/调用者知道它如何处理并发。

    这里是来自 Effective Java 的总结:

    总而言之,每个类都应该清楚地记录其线程安全属性 带有精心措辞的散文描述或线程安全注释。这 synchronized 修饰符在本文档中没有任何作用。有条件的 线程安全类必须记录哪些方法调用序列需要 外部同步,以及执行这些时要获取哪个锁 序列。如果您编写一个无条件线程安全的类,请考虑使用私有的 锁定对象代替同步方法。这可以保护您免受同步 客户和子类的干扰,让您可以灵活地 在以后的版本中采用更复杂的并发控制方法

    【讨论】:

    • “最好的方法是内部锁定。”为什么是最好的?最适合什么?什么时候?如果我在单线程中使用数据结构,我不希望它浪费时间玩锁。
    • 这就是为什么我说如果可以的话,不要使用任何锁定。然后尝试内部锁定,然后外部锁定。并记录您的课程
    • 我认为没有锁定是内部锁定的子类。在这两种情况下,库都会处理事情,调用者不必担心。使用无锁算法的能力确实是内部锁定的一个优点,但我认为这不是一个明确的优势。
    • @ugoren 我不同意,因为它必须在课堂上记录下来。如果我们没有文档,那么是的,它会像您说的那样是“子类”,因为您只是不知道某些东西是否是线程安全的,但是文档会告诉您。 Java 做同样的事情。例如,请参阅 ConcurrentMap 和 Map。
    • 我不确定我们是否同意条款。 “无锁定”是指库是线程安全的,尽管没有使用锁定。内部锁定是指库使用锁来保证线程安全。外部锁定是调用者有责任在适当的锁下(或简单地在单个线程中)调用类。我说第一个是第二个的子类。关于文档,用户不需要关心它们之间的区别。
    【解决方案4】:

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

    1. 这些操作是单独使用还是可以在某些场景中一起使用?
    2. 这些操作是否也可以在单线程环境中使用?

    第 1 点有一些有趣的含义。如果您在内部锁定,那么如果您只使用每个操作本身,那么您是安全的。但是,如果您可能在一个序列中使用它们中的两个或多个,请记住,每个操作的原子性并不能保证整个序列的原子性,因此无论如何都需要外部锁定。例如:

    if(buffer not empty)
        extract from buffer
    

    尽管这两个操作本身都是原子的,但上面的代码显然不是线程安全的。

    第 2 点再次反对内部锁定:在单线程环境中,您不需要锁定,因此通过获取和释放内部锁定会产生不必要的开销。这就是在 Java 中不推荐使用 HashTableVector 类的原因之一。

    【讨论】:

      【解决方案5】:

      Herb Sutter 和 Andrei Alexandrescu 建议以下 [source]

      如果您的应用程序跨线程共享数据,请安全地这样做:

      • 请查阅目标平台的文档以了解本地同步原语
      • 更喜欢将平台的原语包装在您自己的抽象中
      • 确保您使用的类型在多线程程序中可以安全使用
      • 保证非共享对象是独立的
      • 记录调用者需要做什么才能在不同线程中使用该类型的相同对象

      本文讨论了线程安全设计的三种方式:internalexternallock-free,因此您可能会发现它很有用。

      【讨论】:

        【解决方案6】:

        如果只有一个线程在读,只有一个线程在写,则不需要同步,条件是通过在一次操作中写入各自的新值来更新头和尾索引:

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

        通过计算 circ 结构之外的新索引值,您可以确保不会将其他线程与部分值混淆:如果 cirf.tail 直接递增,则测试限制并可能还使用 circ.head 清除线程冒着有两个不同的尾部值可供比较的风险。

        如果有多个线程读取和多个线程写入,我建议您使用自旋锁,因为操作本身所需的时间可能非常短。

        【讨论】:

          【解决方案7】:

          如果您处理数据结构的方法很小,只是一些指令,则根本不应该进行锁定。原子操作是要走的路,在这里。 C++11 和 C11 都为此提供了新的接口。许多编译器已经具有对先前版本标准的扩展等接口。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-05-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2019-12-13
            相关资源
            最近更新 更多