shared_ptr的线程安全性

近期在网上冲浪时看到一篇boost的文章,里面聊到了shared_ptr的线程安全性

https://www.boost.org/doc/libs/1_87_0/libs/smart_ptr/doc/html/smart_ptr.html#shared_ptr_thread_safety

据原文所说,shared_ptr的线程安全性与c++中的容器是一致的。比如说std::map,你可以多线程访问,但是多个线程不能同时析构它,不能同时修改它。同样,shared_ptr可以多线程读,但是不能多线程析构,不能够多线程修改。这里说的shared_ptr是shared_ptr这个class本身的线程安全性与c++的其他容器是一致的,而不是shared_ptr所指向的对象。

一个shared_ptr至少包含了这几个成员:weak_count, ref_count, object_ptr。两个count都是原子类型的,所以对他们递增或递减是不存在多线程问题的。

更准确的说,一个shared_ptr的成员大概是这样的:

template <typename T>
class shared_ptr {
T *object_ptr;
struct Count {
atomic_int weak_count, ref_count;
};
Count *counts;
};

但是!众所周知,多线程线程安全性是不可组合的,所以这里两个原子操作,一旦混合起来就无法确保线程安全性。以前文链接中的example6为例子:

// 注意,这里p3是一个全局变量,所以可以被两个线程访问。
// thread A
p = p3; // reads p3, writes p // thread B
p3.reset(); // writes p3; undefined, simultaneous read/write

这里p3.reset包含2个操作,两个都不是原子的,即object_ptr = nullptr, counts = nullptr。这里假定p3的ref_count是1,weak_count=0,那么如果说example6的执行顺序是这样的:

  1. thread A检查了counts->ref_count,发现不是0
  2. thread B调用reset,发现ref_count==1,所以析构了object_ptr,并设置其为nullptr
  3. thread A设置了counts = p3.counts
  4. thread B由于ref_count1, weak_count0, 释放了counts
  5. thread A尝试递增counts,完蛋了

有一个推荐的做法,即:

void ThreadFunc(shared_ptr p) {
shared_ptr local_copy;
{
lock_guard guard{mutex};
local_copy = p;
}
// 继续操作local_copy
......
}

这样做就符合了shared_ptr的线程安全性了。

cmu15445 Copy On Write的线程安全字典树(trie)

到这里忽然想到了之前做cmu15445的project0的时候也有不少用上shared_ptr的地方,所以想着回顾一下代码,看看有没有什么地方我忽略了线程安全性问题。

首先先介绍一下这个字典树的实现吧。

首先,它使用shared_ptr实现一个线程不安全的trie。假定一开始的trie是这样的:

然后我给插入了一个"ad",那么它会连着root一起复制一遍shared_ptr,原来的trie如虚线所示,新的trie则是蓝色部分:

可以看出,trie是从根开始,一路复制shared_ptr到需要修改的地方的。至于删除则类似。

那么这个trie的线程安全性如何?

看起来都是对原来的trie的读取,包括对shared_ptr的读取操作和每个节点的map<char,shared_ptr>的读取,所有的修改都只是在复制之后的副本上进行修改的。这样一来,只要保证初始被多个线程共享的root这个shared_ptr不会析构即可,这里可以用上前面推荐的操作:

struct Guard {
shared_ptr<TrieNode> guard;
// 为了减少代码,这里用void *这种形式来表现一个值
void *value;
}
shared_ptr<TrieNode> root;
// 这个Get操作有点MVCC的味道,都是读取的时候不会锁住所有写入操作的,都是可以访问历史版本的。
Guard Get(string_view sv) {
shared_ptr local_copy;
{
lock_guard lock{mutex};
local_copy = root;
}
return Guard{local_copy, local_copy->Get(string_view key)};
}
shared_ptr<TrieNode> Put(string_view key, void *value) {
shared_ptr local_copy;
{
lock_guard lock{mutex};
local_copy = root;
}
// 这里的put就是前面说的从根开始进行了复制,直到被修改的地方的操作。
return local_copy->Put(key, value);
}

这样一来线程安全就已经做到了,所有写入都可以做到写时复制。但是奇怪的是为什么project0要求我用一个写锁?

后面看了当时写的代码才想起来,原来project0要求实现的是对前面的线程安全版本的封装,即:

class TrieStore {
......
shared_ptr<TrieNode> Put(string_view key, void *value) {
lock_guard lock{write_lock};
shared_ptr local_copy;
{
lock_guard lock{mutex};
local_copy = root;
}
root = local_copy->Put(key, value);
}
Guard Get(string_view sv) {
shared_ptr local_copy;
{
lock_guard lock{mutex};
local_copy = root;
}
return Guard{local_copy, local_copy->Get(string_view key)};
}
} trie_store;
void ThreadFunc() {
// 给trie_store放入1到65536的key,value都是old+key的形式,比如说1的value是old1,54321的是old54321
// 给trie_store移除1到65536
// 给trie_store放入1到65536,value都是new+key的形式
}
void Test() {
// 起n个线程,每个线程都执行ThreadFunc
// 检查是否有1到65535,每个键是否都是new+key的形式
}

要确保每一个修改都最终可以通过trie_store这个对象确认到,但是所有读取操作都不能被阻塞,所以里面的Put操作需要上一个写锁。

shared_ptr的线程安全性与再论cmu15445 project0的COW线程安全字典树的更多相关文章

  1. Java中各种集合(字符串类)的线程安全性!!!

    Java中各种集合(字符串类)的线程安全性!!! 一.概念: 线程安全:就是当多线程访问时,采用了加锁的机制:即当一个线程访问该类的某个数据时,会对这个数据进行保护,其他线程不能对其访问,直到该线程读 ...

  2. volatile变量能保证线程安全性吗?为什么?

    在谈及线程安全时,常会说到一个变量——volatile.在<Java并发编程实战>一书中是这么定义volatile的——Java语言提供了一种稍弱的同步机制,即volatile变量,用来确 ...

  3. 深度分析Java的枚举类型—-枚举的线程安全性及序列化问题

    原文:深度分析Java的枚举类型--枚举的线程安全性及序列化问题 枚举是如何保证线程安全的 要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是enum吗?答案很明显不是,enum就和clas ...

  4. shared_ptr的线程安全性

    一: All member functions (including copy constructor and copy assignment) can be called by multiple t ...

  5. 《Java并发编程实战》第二章 线程安全性 读书笔记

    一.什么是线程安全性 编写线程安全的代码 核心在于要对状态訪问操作进行管理. 共享,可变的状态的訪问 - 前者表示多个线程訪问, 后者声明周期内发生改变. 线程安全性 核心概念是正确性.某个类的行为与 ...

  6. Java线程安全性中的对象发布和逸出

    发布(Publish)和逸出(Escape)这两个概念倒是第一次听说,不过它在实际当中却十分常见,这和Java并发编程的线程安全性就很大的关系. 什么是发布?简单来说就是提供一个对象的引用给作用域之外 ...

  7. 【Java并发.2】线程安全性

    要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享(Shared)和可变的(Mutable)状态的访问. “共享”意味着变量可以由多个线程同时访问,而“可变”则意味着变量的值在其生 ...

  8. Java并发(理论知识)—— 线程安全性

    1.什么是线程安全性                                                                                      当多个线 ...

  9. Java并发编程原理与实战二十:线程安全性问题简单总结

    一.出现线程安全性问题的条件 •在多线程的环境下 •必须有共享资源 •对共享资源进行非原子性操作   二.解决线程安全性问题的途径 •synchronized (偏向锁,轻量级锁,重量级锁) •vol ...

  10. Java并发编程原理与实战八:产生线程安全性问题原因(javap字节码分析)

    前面我们说到多线程带来的风险,其中一个很重要的就是安全性,因为其重要性因此,放到本章来进行讲解,那么线程安全性问题产生的原因,我们这节将从底层字节码来进行分析. 一.问题引出 先看一段代码 packa ...

随机推荐

  1. Java接口-详解

    一.基本概念 接口(Interface),在JAVA编程语言中是一个抽象类型,是抽象方法的集合.接口通常以interface来声明.一个类通过继承接口的方式,从而来继承接口的抽象方法. 如果一个类只由 ...

  2. Codeforces Round 957 (Div. 3)

    题目链接:Codeforces Round 957 (Div. 3) 总结:E不懂,F差一个set去重 A. Only Pluses fag:枚举 B. Angry Monk fag:模拟 Solut ...

  3. Golang sync.pool源码解析

    Golang sync.pool源码解析 - sync.pool - 是什么 - 怎么用 - demo - 真实世界的使用 - 源码解读-数据结构 - 源码解读-读写流程 - 写流程 - 读流程 - ...

  4. 份额大涨! 天翼云稳居中国公有云laaS市场、laaS+PaaS市场第三!

    近日,国际数据公司(IDC)最新发布的<公有云市场数据跟踪,2023Q3>报告显示,在公有云整体市场增速全面收紧的背景下,中国电信天翼云市场份额大涨,中国公有云IaaS市场份额增长至12. ...

  5. API网关-APISIX简介

    本文分享自天翼云开发者社区<API网关-APISIX简介>,作者:w****n Apache APISIX 是一个动态.实时.高性能的云原生 API 网关,提供了负载均衡.动态上游.灰度发 ...

  6. 如何在M芯片的Mac上爽玩原神

    [热点速递]苹果震撼发布全新M4 Mac mini,国补福利下惊喜价仅约3500元!这不仅是一次办公体验的全新升级,更是对高效能与性价比完美融合的一次致敬.想象一下,以如此亲民的价格,拥抱苹果标志性的 ...

  7. git pull报错:Pulling without specifying how to reconcile divergent branches is discouraged.

    一.保存内容如下 二.翻译 三.设置为默认即可:git config pull.rebase false

  8. 运行jar包时,在命令行中指定依赖的jar包和主类

    在一次实验过程中,使用maven打包java项目为jar包,打出来的myexp.jar包只有7KB(我的实验项目正常打出来的包不小于60MB).这时,运行java -jar myexp.jar报错&q ...

  9. AI 时代 UI 设计的哲学与伦理

    无论是在桌面.移动应用,还是未来可能出现的全新形态中,空间直觉始终是人类在数字世界中导航的根本. 丹尼尔·罗德里格斯 图片来源:维基百科 想象一下,踏入1427年佛罗伦萨圣母玛利亚诺维拉教堂昏暗的光线 ...

  10. Vulnhub-kioptix2014靶机getshell及提权

    靶机搭建 点击扫描虚拟机 然后扫描文件夹即可 信息收集 扫描ip nmap扫描得到目标靶机ip nmap -sn 192.168.108.0/24 故 攻击机:192.168.108.130 目标靶机 ...