【问题标题】:Is this a correct C++11 double-checked locking version with shared_ptr?这是带有 shared_ptr 的正确 C++11 双重检查锁定版本吗?
【发布时间】:2015-06-18 09:41:22
【问题描述】:

Jeff Preshing 的 article 声明双重检查锁定模式 (DCLP) 在 C++11 中是固定的。用于此模式的经典示例是单例模式,但我碰巧有一个不同的用例,我仍然缺乏处理“原子武器”的经验——也许这里有人可以帮助我。

以下代码是 Jeff 在"Using C++11 Sequentially Consistent Atomics" 下描述的正确 DCLP 实现吗?

class Foo {
    std::shared_ptr<B> data;
    std::mutex mutex;

    void detach()
    {
      if (data.use_count() > 1)
      {
        std::lock_guard<std::mutex> lock{mutex};
        if (data.use_count() > 1)
        {
          data = std::make_shared<B>(*data);
        }
      }
    }

public:
  // public interface
};

【问题讨论】:

  • 如果您使用互斥锁保护data,则不需要atomic_store
  • use_countconst,所以按照一般的库规则,并发调用它不会导致数据竞争。
  • @KerrekSB 我在 Herbs talk 中听说了 const 的评论。你真的会假设这条规则得到普遍执行吗?感谢您对不必要的atomic_store 的提示 - 我忽略了这一点。
  • 嗯,[res.on.data.races]/2 要求 const 成员函数不修改对象。但是,这有点不清楚,因为复制data 显然会改变使用次数。但是复制构造函数需要一个 const 值,所以我认为这不构成与数据竞争相关的修改。在实践中,我希望这是无数据竞争的,但据我所知,标准并不完全清楚。
  • 啊,没关系。首先,引用的段落基本上说复制构造函数不会“修改”参数(因为它是 const)。 [util.smartptr.shared]/4 中有一个说明:“为了确定是否存在数据竞争,成员函数应仅访问和修改 shared_ptrweak_ptr 对象本身,而不是它们引用的对象。 use_count() 中的更改不反映可能引入数据竞争的修改。”

标签: c++ multithreading c++11


【解决方案1】:

不,这不是 DCLP 的正确实现。

问题是您的外部检查 data.use_count() &gt; 1 访问对象(类型为B with reference count),可以在互斥保护部分删除(未引用) .任何形式的记忆栅栏都无济于事。

为什么 data.use_count() 访问对象

假设这些操作已经执行:

shared_ptr<B> data1 = make_shared<B>(...);
shared_ptr<B> data = data1;

那么您有以下布局(weak_ptr 支持未在此处显示):

         data1             [allocated with B::new()]         data
                           --------------------------
 [pointer type] ref; -->   |atomic<int> m_use_count;|    <-- [pointer type] ref
                           |B obj;                  |
                           --------------------------

每个shared_ptr 对象只是一个指针,它指向分配的内存区域。这个内存区域嵌入了B 类型的对象加上原子计数器,反映了shared_ptr 的数量,指向给定的对象。当这个计数器变为零时,内存区域被释放(并且B 对象被销毁)。正是这个计数器由shared_ptr::use_count() 返回。

UPDATE:执行,可能导致访问已经释放的内存(最初,两个shared_ptr指向同一个对象,.use_count()为2):

/* Thread 1 */                   /* Thread 2 */       /* Thread 3 */
Enter detach()                   Enter detach()
Found `data.use_count()` > 1     
Enter critical section                                   
Found `data.use_count()` > 1
                                 Dereference `data`,
                                 found old object.
Unreference old `data`,
`use_count` becomes 1 
                                                      Delete other shared_ptr,
                                                      old object is deleted
Assign new object to `data`
                                 Access old object
                                 (for check `use_count`)
                                 !! But object is freed !!

外部检查只需要一个指向对象的指针来决定是否需要获取锁。

顺便说一句,即使你的实现也是正确的,它有一点意义:

  1. 如果可以同时从多个线程访问data(和detach),则对象的唯一性没有任何优势,因为它可以从多个线程访问。如果你想改变对象,所有对data的访问都应该受到外部互斥锁的保护,这样detach()就不能同时执行了。

  2. 如果data(和detach)只能被单线程同时访问,detach的实现根本不需要任何锁定。

【讨论】:

  • 所有玩家,即datamutexdetach()都应该是一个类的成员。因此,如果 detach() 被调用,data 是不可能被删除的。我将通过修改问题来解决这个问题。
  • there is no way, that data gets deleted if detach() is being called。我的意思是,对象引用 data 是未引用的,因为您为data 分配了新值。因此, data.use_count() 可以访问对象,而 shared_ptr 不指向它。这是对 shared_ptr 的滥用,如果同时删除指向该对象的其他 shared_ptr,可能会导致访问已释放的内存。
  • 如果我没记错的话,你的场景只有在线程 1 或线程 2 处理对 Foo 的引用时才可能发生 - 另一个线程拥有的引用。您的示例进一步假设 Foo 的拥有线程将其杀死,而持有引用的线程仍在处理它。如果是这样的话,那么无论如何,有些东西已经严重损坏了。
  • 如果您的data 引用仅由单线程拥有和访问,为什么要使用锁定?请参阅我答案末尾的第 2 点。
  • 我从来没有说过该对象不应该被多个线程使用,但绝对不应该在其中一个线程访问它时删除它 - 没有针对这种用例的保护。更清楚地说,我针对Foo 的用例是一个写时复制实现,它利用了std::shared_ptr 已经以线程安全方式引用计数的事实。最大的缺陷是在Foo 上的非常量成员函数调用上进行线程安全分离。没有互斥锁的一个问题是,例如创建多个副本就足够了。
【解决方案2】:

如果两个线程同时在 Foo 的同一实例上调用 detach,这将构成数据竞争,因为 std::shared_ptr&lt;B&gt;::use_count()(只读操作)将与 std::shared_ptr&lt;B&gt; 移动赋值运算符(a修改操作),这是一个数据竞争,因此是未定义行为的原因。另一方面,如果 Foo 实例永远不会同时访问,则不会存在数据竞争,但 std::mutex 在您的示例中将毫无用处。问题是:data 的指针是如何被共享的?如果没有这一关键信息,即使从未同时使用 Foo,也很难判断代码是否安全。

【讨论】:

  • 我最初使用此代码std::atomic_store(&amp;data, std::make_shared&lt;B&gt;(*other.data)); 而不是移动分配。如果多个线程创建Foo 的副本,您可能是对的,我们需要根据std::atomic_load/store 来实现Foo 的构造、复制和分配。也许其他人可以在我编辑帖子之前确认这一点? @KerrekSB 建议 'std::atomic_store' 是不必要的,但至少在考虑整个班级时我不确定了。
【解决方案3】:

根据您的消息来源,我认为您仍然需要在第一次测试之前和第二次测试之后添加线程围栏。

std::shared_ptr<B> data;
std::mutex mutex;

void detach()
{
  std::atomic_thread_fence(std::memory_order_acquire);
  if (data.use_count() > 1)
  {
    auto lock = std::lock_guard<std::mutex>{mutex};
    if (data.use_count() > 1)
    {
      std::atomic_thread_fence(std::memory_order_release);
      data = std::make_shared<B>(*data);
    }
  }
}

【讨论】:

  • 我认为std::shared_ptr::use_count() 相当于对std::atomic&lt;long&gt;::load() 的内部调用。如果这是真的,那么这个section 应该适用并且不需要围栏。还是我错过了什么?
  • @HaukeHeibel 我不知道,我没有发现任何支持或反驳的参考资料。你有吗?
  • @Nielk 如果在第一个if 之后,另一个线程将weak_ptr 提升到datashared_ptr,会发生什么?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多