前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >std::shared_ptr 的线程安全性 & 在多线程中的使用注意事项

std::shared_ptr 的线程安全性 & 在多线程中的使用注意事项

作者头像
JoeyBlue
发布2023-03-16 21:09:15
2K0
发布2023-03-16 21:09:15
举报
文章被收录于专栏:代码手工艺人代码手工艺人

我们在讨论 std::shared_ptr 线程安全时,讨论的是什么?

在讨论之前,我们先理清楚这样的一个简单但却容易混淆的逻辑。 std::shared_ptr 是个类模版,无法孤立存在的,因此实际使用中,我们都是使用他的具体模版类。这里使用 std::shared_ptr 来举例,我们讨论的时候,其实上是在讨论 std::shared_ptr 的线程安全性,并不是 SomeType 的线程安全性。

那我们在讨论某个操作是否线程安全的时候,也需要看具体的代码是作用在 std::shared_ptr 上,还是 SomeType 上。

举个例子:

代码语言:javascript
复制
#include <memory>

struct SomeType {
  void DoSomething() {
    some_value++;
  }

  int some_value;
};

int main() {
  std::shared_ptr<SomeType> ptr;
  ptr->DoSomething();
  return 0;
}

这里例子中,如果 ptr->DoSomething () 是运行在多线程中,讨论它是否线程安全,如何进行判断呢?

首先它可以展开为 ptr.operator->()->DoSomething(),拆分为两步:

  1. ptr.operator->() 这个是作用在 ptr 上,也就是 std::shared_ptr 上,因此要看 std::shared_ptr->() 是否线程安全,这个问题后面会详细来说
  2. ->DoSomething () 是作用在 SomeType* 上,因此要看 SomeType::DoSomething () 函数是否线程安全,这里显示是非线程安全的,因为对 some_value 的操作没有加锁,也没有使用 atomic 类型,多线程访问就出现未定义行为(UB)

std::shared_ptr 线程安全性

我们来看看 cppreference 里是怎么描述的:

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

我们可以得到下面的结论:

  1. 多线程环境中,对于持有相同裸指针的 std::shared_ptr 实例,所有成员函数的调用都是线程安全的。
    • 当然,对于不同的裸指针的 std::shared_ptr 实例,更是线程安全的
    • 这里的 “成员函数” 指的是 std::shared_ptr 的成员函数,比如 get ()、reset ()、 operrator->() 等)
  2. 多线程环境中,对于同一个 std::shared_ptr 实例,只有访问 const 的成员函数,才是线程安全的,对于非 const 成员函数,是非线程安全的,需要加锁访问。

首先来看一下 std::shared_ptr 的所有成员函数,只有前 3 个是 non-const 的,剩余的全是 const 的:

成员函数

是否 const

operator=

non-const

reset

non-const

swap

non-const

get

const

operator*、operator->

const

operator

const

use_count

const

unique(until C++20)

const

operator bool

const

owner_before

const

use_count

const

我们来看两个例子 例 1:

代码语言:javascript
复制
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
#include <atomic>
using namespace std;

struct SomeType {
  void DoSomething() {
    some_value++;
  }

  int some_value;
};

int main(int argc, char *argv[]) {
  auto test = std::make_shared<SomeType>();
  std::vector<std::thread> operations;
  for (int i = 0; i < 10000; i++) {
    std::thread([=]() mutable {  //<<--
      auto n = std::make_shared<SomeType>();
      test.swap(n);
    }).detach();
  }

  using namespace std::literals::chrono_literals;
  std::this_thread::sleep_for(5s);
  return 0;
}

例 2:

代码语言:javascript
复制
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
#include <atomic>
using namespace std;

struct SomeType {
  void DoSomething() {
    some_value++;
  }

  int some_value;
};

int main(int argc, char *argv[]) {
  auto test = std::make_shared<SomeType>();
  std::vector<std::thread> operations;
  for (int i = 0; i < 10000; i++) {
    std::thread([&]() mutable {  // <<---
      auto n = std::make_shared<SomeType>();
      test.swap(n);
    }).detach();
  }

  using namespace std::literals::chrono_literals;
  std::this_thread::sleep_for(5s);
  return 0;
}

这两个的区别只有传入到 std::thread 的 lambda 的捕获类型,一个是 capture by copy, 后者是 capture by reference,哪个会有线程安全问题呢?

根据刚才的两个结论,显然例 1 是没有问题的,因为每个 thread 对象都有一份 test 的 copy,因此访问任意成员函数都是线程安全的。 例 2 是有数据竞争存在的,因为所有 thread 都共享了同一个 test 的引用,根据刚才的结论 2,对于同一个 std::shared_ptr 对象,多线程访问 non-const 的函数是非线程安全的。 这个的 swap 改为 reset 也一样是非线程安全的,但如果改为 get () 就是线程安全的。

这里我们打开 Thread Sanitizer 编译例 2(clang 下是 -fsanitize=thread 参数),运行就会 crash 并告诉我们出现数据竞争的地方。

代码语言:javascript
复制
==================
WARNING: ThreadSanitizer: data race (pid=11868)
  Read of size 8 at 0x00016ba5f110 by thread T2:
    #0 std::__1::enable_if<(is_move_constructible<SomeType*>::value) && (is_move_assignable<SomeType*>::value), void>::type std::__1::swap<SomeType*>(SomeType*&, SomeType*&) swap.h:38 (Untitled 4:arm64+0x1000061a8)
    #1 std::__1::shared_ptr<SomeType>::swap(std::__1::shared_ptr<SomeType>&) shared_ptr.h:1045 (Untitled 4:arm64+0x100006140)
    #2 main::$_0::operator()() Untitled 4.cpp:22 (Untitled 4:arm64+0x1000060d4)
    #3 decltype(static_cast<main::$_0>(fp)()) std::__1::__invoke<main::$_0>(main::$_0&&) type_traits:3918 (Untitled 4:arm64+0x100005fc8)
    #4 void std::__1::__thread_execute<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, main::$_0>(std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, main::$_0>&, std::__1::__tuple_indices<>) thread:287 (Untitled 4:arm64+0x100005ec4)
    #5 void* std::__1::__thread_proxy<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, main::$_0> >(void*) thread:298 (Untitled 4:arm64+0x100004f90)

  Previous write of size 8 at 0x00016ba5f110 by thread T1:
    #0 std::__1::enable_if<(is_move_constructible<SomeType*>::value) && (is_move_assignable<SomeType*>::value), void>::type std::__1::swap<SomeType*>(SomeType*&, SomeType*&) swap.h:39 (Untitled 4:arm64+0x1000061f0)
    #1 std::__1::shared_ptr<SomeType>::swap(std::__1::shared_ptr<SomeType>&) shared_ptr.h:1045 (Untitled 4:arm64+0x100006140)
    #2 main::$_0::operator()() Untitled 4.cpp:22 (Untitled 4:arm64+0x1000060d4)
    #3 decltype(static_cast<main::$_0>(fp)()) std::__1::__invoke<main::$_0>(main::$_0&&) type_traits:3918 (Untitled 4:arm64+0x100005fc8)
    #4 void std::__1::__thread_execute<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, main::$_0>(std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, main::$_0>&, std::__1::__tuple_indices<>) thread:287 (Untitled 4:arm64+0x100005ec4)
    #5 void* std::__1::__thread_proxy<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, main::$_0> >(void*) thread:298 (Untitled 4:arm64+0x100004f90)
...

SUMMARY: ThreadSanitizer: data race swap.h:38 in std::__1::enable_if<(is_move_constructible<SomeType*>::value) && (is_move_assignable<SomeType*>::value), void>::type std::__1::swap<SomeType*>(SomeType*&, SomeType*&)

...

ThreadSanitizer: reported 4 warnings
Terminated due to signal: ABORT TRAP (6)

从错误信息中可以清晰地看到出现的数据竞争,在 22 行,也就是调用 swap () 的行。 如果确实需要在多线程环境下对同一 std::shared_ptr 实例做 swap () 操作,可以调用 atomic 对 std::shared_ptr 的重载函数,如:

代码语言:javascript
复制
template< class T >
std::shared_ptr<T> atomic_exchange( std::shared_ptr<T>* p,
                                    std::shared_ptr<T> r);
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-12-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 我们在讨论 std::shared_ptr 线程安全时,讨论的是什么?
  • std::shared_ptr 线程安全性
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档