多读单写类的线程安全性

4

我正在处理一个经常被读取但很少被写入的集合。

class A {
  boost::shared_ptr<std::set<int> > _mySet;
public:
  void add(int v) {
    boost::shared_ptr<std::set<int> > tmpSet(new std::set<int>(*_mySet));
    tmpSet->insert(v);  // insert to tmpSet
    _mySet = tmpSet;    // swap _mySet
  }
  void check(int v) {
    boost::shared_ptr<std::set<int> > theSet = _mySet;
    if (theSet->find(v) != theSet->end()) {
      // do something irrelevant
    }
  }
};

在该类中,add() 方法只被一个线程调用,而 check() 方法则被多个线程调用。check() 方法不会关心 _mySet 是否是最新的。该类是否是线程安全的呢?执行 check() 方法的线程是否可能观察到在 insert to tmpSet 之前发生的 swap _mySet 呢?

如果shared_ptr的交换是原子的,那么我认为这将是线程安全的。否则,在add的末尾可能会有部分交换,而在check的开头可能会有部分复制。你可以用一个真正的原子指针类型类来替换shared_ptr,然后这个算法就是正确的。 - user406009
3个回答

2
您需要同步,它不是线程安全的。通常情况下,即使是像 shared += value; 这样简单的操作也不是线程安全的。
关于 shared_ptr 的线程安全性,请看这里的例子:Is boost shared_ptr <XXX> thread safe? 我还要质疑您在 add() 中的分配/交换以及在 check() 中使用 shared_ptr
更新:
我回头重新阅读了 shared_ptr 的文档...由于 shared_ptr 的引用计数是线程安全的,在您的特定情况下它很可能是线程安全的。然而,我认为您没有必要使用读/写锁来增加不必要的复杂度。

我理解为什么shared += value不是线程安全的,因为它是一个读取-修改-写入操作。但是对于指针的复制和交换是否线程安全呢?因为指针所引用的对象可能处于不一致状态或其他原因。在check()中的shared_ptr确保find()和end()应用于同一对象。这里有什么问题吗? - paper
1
@Anycorn:引用计数本身是线程安全的,但是shared_ptr的复制、移动或赋值操作与引用计数不同步,因此这些操作在“volatile” shared_ptr上不是原子操作。看来你最初的答案是正确的,但更新是错误的 :) - user396672

2
这是使用shared_ptr实现线程安全的有趣用法。是否可行取决于boost::shared_ptr的线程安全保证。特别地,它是否建立了某种栅栏或membar,以便您确保在set的构造函数和insert函数中的所有写操作在指针值的任何修改变得可见之前都已经发生。
我在Boost智能指针的文档中找不到任何线程安全保证。这让我感到惊讶,因为我确信有一些。但是快速查看1.47.0的源代码没有发现任何线程安全保证,而且在多线程环境中使用boost::shared_ptr将失败。(请问有人能告诉我我错过了什么吗?我无法相信boost::shared_ptr忽略了线程。)
总之,有三种可能性:您不能在多线程环境中使用共享指针(似乎是这种情况),共享指针在多线程环境中确保其自身的内部一致性,但不与其他对象建立顺序关系,或者共享指针建立完整的顺序关系。只有在最后一种情况下,您的代码才是安全的。在第一种情况下,您需要在所有内容周围使用某种形式的锁定,在第二种情况下,您需要某种形式的栅栏或membar来确保必要的写操作实际上是在发布新版本之前完成的,并且它们将在尝试读取它之前被看到。

显然,最近的 Boost 库支持 shared_ptr 上的 atomic_load/atomic_store 操作,但它们的实现似乎使用自旋锁,这是一个有争议的选择,从性能角度来看(请参见我的答案)。 - user396672
我在http://www.boost.org/doc/libs/1_48_0/boost/smart_ptr/shared_ptr.hpp中没有看到atomic_load/store的volatile变量,我不明白为什么他们显然使用自旋锁但不提供volatile变量。 - user396672
@user396672 要获得锁定,您必须对所有访问shared_ptr使用atomic_storeatomic_load。否则,就绝对没有保护:operator=基于swap,而swap独立交换两个指针。(事实上,这是经典的保证:在原始代码中,一个线程修改智能指针,其他线程访问它,因此需要某种外部同步。) - James Kanze
你是对的,所有访问操作都应该是原子的(在我的回答中,我忘记了在set构造函数中使用u*_mySet参数,该参数应该是*atomic_load(_mySet))。 - user396672

0

最终,这段代码应该是线程安全的:

atomic_store(&_my_set,tmpSet);

theSet = atomic_load(&_mySet);

(而不是简单的赋值)

但我不知道shared_ptr的原子性支持的当前状态。

请注意,以无锁方式为shared_ptr添加原子性是非常困难的事情;因此,即使实现了原子性,它也可能依赖于互斥锁或用户模式自旋锁,并且可能会受到性能问题的影响。

编辑:也许,_my_set成员变量的volatile限定符也应该被添加...但我不确定它是否严格要求原子操作的语义。


我不会期望atomic_storeatomic_load在智能指针上被定义。标准要求实现基本类型和(原始)指针,但不包括其他内容。除非我漏掉了什么(很可能),没有类类型(甚至浮点类型)支持原子操作。正如你所说,shared_ptr的实现方式是使用某种形式的锁来实现"原子性",那么你最好明确地表示它。 - James Kanze
1
@James Kanze:标准有一个特殊的段落(20.7.2.5 shared_ptr atomic access),列出了shared_ptr的原子函数专业化及其含义(我不确定这些专业化是必需还是可选的),cppreference也将原子作为shared_ptr描述的一部分(http://en.cppreference.com/w/cpp/memory/shared_ptr),boost目前实现了原子加载/存储(尽管未记录)。我相信shared_ptr的原子支持将普遍可用(虽然我怀疑它现在是否广泛可用)。 - user396672
我曾经是标准化工作的积极参与者,我的理解是原子函数的目的是为无锁算法提供可移植支持,并且意图仅为那些可以在没有锁的情况下进行更新和读取(但需要栅栏或内存屏障)的类型提供它们。然而,这可能已经改变了。但我真的看不出有任何兴趣将它们用于其他类型;如果需要锁定,则最好由客户端代码提供。 - James Kanze
@James:我基本上同意,但shared_ptr并不是一个普通的“其他类型”,而且显然有一些兴趣存在(同时,楼主的问题间接证实了这种兴趣)。不幸的是,似乎没有广泛(且免费)可用的无锁实现,这个问题已经足够困难且专利保护。我同意基于锁或自旋锁的权宜之计相当难以使用,但它们的出现表达了一种趋势,所以仍然有一些希望 :) - user396672

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接