线程安全性

关于 std::shared_ptr 的线程安全性, cppreference 中提到:

All member functions (including copy constructor and copy assignment) can be called by multiple threads on different shared_ptr objects without additional synchronization even if these objects are copies and share ownership of the same object. If multiple threads of execution access the same shared_ptr object without synchronization and any of those accesses uses a non-const member function of shared_ptr then a data race will occur; the std::atomic can be used to prevent the data race.

前面一句话的意思是“即使不同线程中的不同 shared_ptr 指向相同对象,也可以安全地调用所有成员函数”。后面一句话的意思是“如果在多个线程中访问同一个 shared_ptr 指针,其中有线程使用了非 const 成员,那么会出现发生数据争用,此时可以使用 std::atomic<shared_ptr>”。

也就是说下面的代码是线程安全的,尽管不能确定对象是在哪个线程中被释放,但是可以保证对象只被释放一次:

int main() {
  std::shared_ptr<int> p1(new int(42));

  std::thread t1([p1]() {
    // p1 和 p2 指向同一个对象
    std::shared_ptr<int> p2 = p1;

    // 释放 p1 和 p2
    p1.reset();
    p2.reset();
  });

  // 释放 p1
  p1.reset();
  t1.join();
  return 0;
}

但下面代码不是线程安全的,因为在两个线程中访问了同一个 std::shared_ptr 指针:

int main() {
  std::shared_ptr<int> p1(new int(42));

  std::thread t1([&p1]() {
    // 使用 p1
    cout << p1.use_count() << endl;
  });

  // 释放 p1
  p1.reset();
  t1.join();
  return 0;
}

实现原理

实现上通常是通过原子操作来保障线程安全性的,如以下伪代码所示,通过原子操作可以保证只有一个线程进入析构步骤:

template<class T>
class shared_ptr {
public:
  ~shared_ptr() {
    // 将引用计数原子地减一,返回原先的值
    if (shared_count_->fetch_sub(1) == 1) {
      // 只会有一个线程进入这个步骤
      delete data_;
      delete shared_count_;     // 这一步会有线程安全问题吗?
    }
  }

private:
  T* data_;
  std::atomic_int* shared_count_;
};

上述代码中 delete shared_count_ 可能引起疑问,如果在线程 A 中执行了这一句,在线程 B 中又试图访问 shared_count_ 应该会有线程安全问题。

但这种情况只会出现在 “多个线程访问同一个 shared_ptr 的场景下”,如果我们遵循正确的使用方式,让线程 A 和 B 持有各自的 shared_ptr 拷贝,那么线程 A 和 B 同时进入析构函数的情况下,shared_count_ 至少是 2,只有在线程 B 已经将其减为 1 的情况下,线程 A 才有机会执行到 delete shared_count_ 这一句。所以上述代码不存在线程安全问题。

总的来说,向其它线程传递 std::shared_ptr 时应该以拷贝的方式传递,这样就不会有线程安全问题。

应用

以上内容表明在多个线程中通过不同 std::shared_ptr 指向同一个对象是线程安全的。当然这是针对 std::shared_ptr 本身而言,对于它所指向的对象还是需要加锁的。

一种有趣的应用是利用 std::shared_ptr 的引用计数及自定义析构函数来判断并行的一批任务是否全部完成:

void runOnAllThreads(std::function<void()> all_threads_complete_cb) {
  std::shared_ptr<int> cb_guard(new int(42), [all_threads_complete_cb](int* obj) {
    // 自定义析构函数
    // 对象被析构意味着所有线程执行完毕
    all_threads_complete_cb();
    delete obj;
  });

  for (auto& thread : threads) {
    thread.post([cb_guard]() {
      // 每个线程执行后都触发这个回调
      // 全部线程执行完毕后, cb_guard 的引用计数减为 0, 触发析构函数
    });
  }
}