当前位置: 首页 > news >正文

原子操作及基于原子操作的shared_ptr实现

什么是原子操作

在多线程或并发编程中,对于某个原子操作,该操作要么完全执行,要么完全不执行,不会处于中间状态,其也中间状态不会被看到。

如何保证原子操作的原子性

单处理器单核

因为只有一个核心操作内存空间,因此只需要保证对应原子操作不被打断即可。

实现:底层自旋锁、屏蔽中断

多处理器多核

在多处理器多核的情况下,同时存在多个cpu核心访问内存,为了保证原子性,除了保证原子操作不被打断外,还需要保证内存的变量的中间状态不被其他核心看到,因此需要禁止对相关内存的访问。

存储体系结构

cpu 缓存

cpu缓存:为了解决cpu计算速度与内存访问速度不匹配的问题,通过设置多级缓存实现,越靠近cpu的缓存,访问速度越快,但容量越小。

因为缓存比主存要小,因此需要采用LRU或其他策略对缓存中的内容进行更新,但是这也可能导致缓存不命中,即缓存中没有需要操作的内存的内容。

缓存命中以cache line为单位

写回策略

如果命中,则直接写在缓存中,并标记为脏

如果不命中,需要采用LRU策略定位一块缓存块,进行替换:

  • 若定位的缓存块为脏数据,则先将脏数据刷主存,然后将对应缓存块进行替换,并写入缓存,标记为脏
  • 若定位的缓存块不为脏数据,则直接替换,然后写入缓存,标记为脏

如果命中,则直接读缓存

如果不命中,需要采用LRU策略定位一块缓存块,进行替换:

缓存一致性问题

cpu是多处理器多核的,每个cpu核心的缓存是独立的,基于写回策略会导致每个cpu的缓存可能不一致

  • 若定位的缓存块为脏数据,则先将脏数据刷主存,然后将对应缓存块进行替换,并读缓存,标记为非脏
  • 若定位的缓存块不为脏数据,则直接替换,然后读缓存,标记为非脏

如何解决

总线嗅探 bus snooping

核心会监听缓存块的状态,如果发生改变,则会通知每个核心,从而避免其他核心对相关内存的访问。

存在问题

通知基于总线传播,由于距离不同,存在先后顺序的问题

事务的串行化

先监听的先收到通知

总线带宽优化

基于总线嗅探和事务串行化,解决了缓存不一致问题,但频繁的通知会造成总线压力,有些通知是没有必要的,因此可以优化。

缓存一致性协议 MESI

MESI 协议是一个基于失效的缓存一致性协议,支持 write-back 写回缓存的常用协议。 主要原理:通过总线嗅探策略(将读写请求通过总线广播给所 有核心,核心根据本地状态进行响应

仅对Modified和Exclusive状态的缓存块进行通知

原子变量

原子变量是一种多线程编程中常用的同步机制。它能确保对共 享变量的操作在执行时不会被其他线程的操作干扰,从而避免 竞态条件。 原子变量具备原子性,也就是要么全部完成,要么全部未完 成。

  • c/c++ 标准库提供了丰富的原子类型。
  • std::atomic is_lock_free:是否支持无锁操作;
  • store(T desired, std::memory_order order):用于将指 定的值存储到原子对象中; load(std::memory_order order):用于获取原子变量的当前 值。
  • exchange(std::atomic* obj, T desired):访问和修改 包含的值,将包含的值替换并返回它前面的值。如果替换成功, 则返回原来的值。
  • compare_exchange_weak(T& expected, T val, memory_order success, memory_order failure):比较一 个值和一个期望值是否相等,如果相等则将该值替换成一个新 值,并返回 true;否则不做任何操作并返回 false。注意, compare_exchange_weak 函数是一个弱化版本的原子操作函 数,因为在某些平台上它可能会失败并重试。如果需要保证严格 的原子性,则应该使用 compare_exchange_strong 函数。
  • compare_exchange_strong(T& expected, T val, memory_order success, memory_order failure) fetch_add
  • fetch_sub
  • fetch_add
  • fetch_or
  • fetch_xor

内存序

编译器会优化代码,导致代码改变,cpu会优化指令,导致指令改变,通过内存序的控制可以设置一个线程对内存的更新何时被其他线程看见,同时设置优化的范围

为了控制编译器优化重排和cpu指令优化重拍,提供了6种内存模型控制内存序

memory_order_relaxed:只保证原子操作,不保证内存序,即读取的内容不一定最新,写的内容不一定被其他线程读取

松散内存序,只用来保证对原子对象的操作是原子的,在不需要保证顺序时使用;

memory_order_release:对应写操作,写入缓存时,需要同步到内存种,同时前面的代码不能优化到原子操作的后面

释放操作,在写入某原子对象时, 当前线程的任何前面的读写操作都不允许重排到这个操作的后面 去,并且当前线程的所有内存写入都在对同一个原子对象进行获取的其他线程可见;通常与 memory_order_acquire 或 memory_order_consume 配对使用

memory_order_acquire:对应读操作,读缓存时直接从内存中读,同时后面的操作不能优化到前面。

获得操作,在读取某原子对象时, 当前线程的任何后面的读写操作都不允许重排到这个操作的前面 去,并且其他线程在对同一个原子对象释放之前的所有内存写入 都在当前线程可见

memory_order_consume

同 memory_order_acquire 类 似,区别是它仅对依赖于该原子变量操作涉及的对象,比如这个 操作发生在原子变量 a 上,而 s = a + b;那 s 依赖于 a,但 b 不 依赖于 a;当然这里也有循环依赖的问题,例如:t = s + 1,因 为 s 依赖于 a,那 t 其实也是依赖于 a 的;在大多数平台上,这 只会影响编译器的优化;不建议使用

memory_order_acq_rel:对应读写操作,对应原子操作前面的代码都不能优化后面,后面的代码不能优化到前面

获得释放操作,一个读‐修改‐写操作 同时具有获得语义和释放语义,即它前后的任何读写操作都不允 许重排,并且其他线程在对同一个原子对象释放之前的所有内存 写入都在当前线程可见,当前线程的所有内存写入都在对同一个 原子对象进行获取的其他线程可见;

memory_order_seq_cst:全局代码都不能优化

顺序一致性语义,对于读操作相当 于获得,对于写操作相当于释放,对于读‐修改‐写操作相当于获 得释放,是所有原子操作的默认内存序,并且会对所有使用此模 型的原子操作建立一个全局顺序,保证了多个原子变量的操作在 所有线程里观察到的操作顺序相同,当然它是最慢的同步模型

基于原子操作的shared_ptr实现


#pragma once#include <atomic>// shared_ptr<int> p1(new int(42));
// shared_ptr<int> p2 = shared_ptr<int>(new int(42));
// shared_ptr<int> p3 = p1;// shared_ptr<int> p2 = new int(42);
class A {
public:void func() {}
};// shared_ptr<A> p1(new A());
// p1->func();
template <typename T>
class shared_ptr {
public://默认构造shared_ptr() : ptr_(nullptr), ref_count_(nullptr) {}//构造函数,初始化引用计数器为1explicit shared_ptr(T* ptr) : ptr_(ptr), ref_count_(ptr ? new std::atomic<std::size_t>(1) : nullptr) {}~shared_ptr() {release();}//拷贝构造函数,引用计数器加1shared_ptr(const shared_ptr<T>& other) : ptr_(other.ptr_), ref_count_(other.ref_count_) {if (ref_count_) {//对原子变量,执行加1的原子操作,同时内存序为memory_order_relaxed,即不要求将当前的写入操作同步到内存中,//也不要求将最新原子变量的值进行同步//这是因为当前原子操作的前后代码,并不会因为原子变量的值不同而产生影响ref_count_->fetch_add(1, std::memory_order_relaxed);}}//赋值运算符//释放原先的shared_ptr,同时对新的shared_ptr的计数器加1shared_ptr<T>& operator=(const shared_ptr<T>& other) {if (this != &other) {release();ptr_ = other.ptr_;ref_count_ = other.ref_count_;if (ref_count_) {//同样采用memory_order_relaxed内存序,对原子变量加1ref_count_->fetch_add(1, std::memory_order_relaxed);}}return *this;}// noexcept: the function will not throw exceptions// 编译期会生成更高效的代码,不需要为异常处理生成额外的代码// STL //移动构造,将shared_ptr转移,因此计数器无需增加shared_ptr<T>(shared_ptr<T>&& other) noexcept : ptr_(other.ptr_), ref_count_(other.ref_count_) {other.ptr_ = nullptr;other.ref_count_ = nullptr;}//移动赋值运算符//同理,计数器无需增加shared_ptr<T>& operator=(shared_ptr<T>&& other) noexcept {if (this != &other) {//自赋值检查,即自己不能赋值给自己release();ptr_ = other.ptr_;ref_count_ = other.ref_count_;other.ptr_ = nullptr;other.ref_count_ = nullptr;}return *this;}// *p1//实现解引用运算符的功能T& operator*() const {return *ptr_;}// p1->func()//实现->运算符的功能T* operator->() const {return ptr_;}std::size_t use_count() const {//采用memory_order_acquire内存序,因为需要返回最新的值return ref_count_ ? ref_count_->load(std::memory_order_acquire) : 0;}T* get() const {return ptr_;}void reset(T * p = nullptr) {release();ptr_ = p;ref_count_ = p ? new std::atomic<std::size_t>(1) : nullptr;}private://释放指针,先对计数器减一,如果计数器为0则,真的释放指针呢个,否则说明当前shared_ptr还存在引用,因此不能释放void release() {//使用memory_order_acq_rel内存序,acq确保执行减1操作前获得最新的值,rel确保减1操作之后代码不会被重排到减1操作之前if (ref_count_ && ref_count_->fetch_sub(1, std::memory_order_acq_rel) == 1) {delete ptr_;delete ref_count_;}}T* ptr_;//当前shared_ptr管理的指针std::atomic<std::size_t>* ref_count_;//引用计数器,共享的原子变量,用于计数当前指针的引用计数,因为要被多线程共同访问,因此需要分配在堆上
};

https://github.com/0voice

http://www.dtcms.com/a/336535.html

相关文章:

  • PYTHON让繁琐的工作自动化-PYTHON基础
  • 【撸靶笔记】第五关:GET - Double Injection - Single Quotes - String
  • 基于STM32单片机智能RFID刷卡汽车位锁桩设计
  • Qt同步处理业务并禁用按钮
  • linux系统------kubenetes单机部署
  • LeetCode 分类刷题:2962. 统计最大元素出现至少 K 次的子数组
  • 5G虚拟仿真平台
  • [激光原理与应用-292]:理论 - 波动光学 - 驻波的本质是两列反向传播的相干波通过干涉形成的能量局域化分布
  • 安全多方计算(MPC)简述
  • Compose笔记(四十六)--Popup
  • Houdini 粒子学习笔记
  • 服装外贸管理软件 全流程优化解决方案
  • 学习记录(二十)-Overleaf如何插入参考文献
  • Chrome 插件开发实战:从入门到上架的全流程指南
  • 最长回文子串问题:Go语言实现及复杂度分析
  • 63.不同路径
  • Django前后端交互实现用户登录功能
  • 计算机网络---跳板机与堡垒机
  • Centos 更新/修改宝塔版本
  • 第七十八章:AI的“智能美食家”:输出图像风格偏移的定位方法——从“滤镜病”到“大师风范”!
  • 点云的PFH 和 FPFH特征
  • k8sday09
  • C# 反射和特性(自定义特性)
  • 股票术语:“支撑位”
  • 解码词嵌入向量的正负奥秘
  • 一张图总结 - AI代理上下文工程:构建Manus的经验教训
  • Python多线程、锁、多进程、异步编程
  • Linux | i.MX6ULL网络通信-套字节 TCP(第十七章)
  • 【k8s】Kubernetes核心概念与架构详解
  • 4.8 Vue 3: provide / inject 详解