wordpress js插件开发教程,seo排行榜,网站开发项目计划书模板,商圈数据app原子操作 原子操作原理什么是原子操作#xff1f;原子性原子变量相关接口内存序 shared_ptr的实现 原子操作原理
什么是原子操作#xff1f;
原子操作其实就是指在多线程的环境下#xff0c;确保对共享变量的操作不会被干扰#xff0c;从而避免了竞态条件。
我们都知道原子性原子变量相关接口内存序 shared_ptr的实现 原子操作原理
什么是原子操作
原子操作其实就是指在多线程的环境下确保对共享变量的操作不会被干扰从而避免了竞态条件。
我们都知道在多线程环境下对于共享变量的操作是存在线程安全的问题的假设当前存在一个全局变量 count 此时创建多个线程对 count 进行 操作就会诱发线程安全的问题对于 count 这种临界资源来说表面上我们看着只有一句代码但是解释成汇编语句他就会存在多个操作 多线程环境下就存在竞态条件多个线程去竞争 count 这一个临界资源对于单条汇编语句他的操作必定是原子性的但是在多条汇编语句下随着线程的切换 count 变量的操作就充满了不确定性。
而原子变量就可以有效的解决掉这个问题他保证了对于 count 这个变量的操作比如说现在有两个线程线程 A 对 count 变量操作的时候他的操作是不可被打断的只有当线程 A 对 count 变量操作完成以后线程 B 才会去获取到这个资源进行他的操作这就保证了 count 操作的原子性。
原子性
理解原子性我们首先的从 CPU 的存储体系架构下手在当前这个多处理器多核心工作的架构下必定会存在 CPU 资源竞争的发生因为我们的线程就是在核心上运行的竞态条件的发生就会意味着切换就会有线程安全的情况发生。
首先我们来了解一下CPU 的存储体系架构如下图所示 因为在当前时代下CPU 的读取速度时远高于主存的就出现了缓存cache的概念我们可以理解为他是 CPU 与主存之间的一种存储结构他的读取速度高于主存略低于 CPU 主要分为 3 级分为 L1L2L3前两级 L1L2 是 CPU 核心所独有L3 是所有 CPU 核心共享的。
原子性就是指在当前核心上的操作的中间状态并不被其他核心所识别到就如下图所示 而我们有如何做到这种状态呢
如果是单处理器单核心的情况下就比较简单因为就算是创建多个线程每次也只会允许一个线程进行运转主要我们屏蔽掉中断机制使用底层自旋锁我们就可以去做到这种原子性的操作但是注意这只是在单处理器单核心下当前开发环境大多数都是在多处理器多核心的情况下。
在多处理器多核心的场景下就会存在 CPU 资源被争夺的情况产生要保证原子性就需要保证其他的核心不去操作当前核心操作的内存空间对于L1L2缓存来说当前核心独有我们并不需要做过多的操作但是对于 L3 缓存来说是所有核心所共有的在我们当前的工作模式下有一种总线嗅探机制就是最终的数据会通过总线传递给其他的核心。
以往 0x86 是通过锁总线的方式避免所有内存的访问但是这就会造成一个问题就是效率低下的问题只有当前核心可以工作其他核心都被阻拦明显效率就变低了。现在采用的是阻止其他核心对相关的内存空间访问。 CPU 如何读写数据 首先我们来看写策略对于写策略通常会存在以下两种处理方式
直写这种策略其实很简单当 CPU 需要写入数据时此时就会将数据同时都写入到 cache 和主存当中也就意味着其他核心每一次读取到的数据都是最新的就不会出现不同步的问题。写回当 CPU 需要写入数据时此时并不是将数据立即就写入到主存当中而是先写入缓存当中缓存当中是以 cache line 去保存数据的其中有一个标记位我们写入数据以后如果发生缓存命中此时就可以直接写如果没有命中就会去缓存中定位一个数据块将数据进行写入同时标记标志位为 dirty 脏数据。当我们在缓存中定位数据块是会出现两种情况一种是缓存中所有的数据块都已经被标记没有空闲的此时就会就会通过 LRU 策略最久没有被使用去淘汰掉一个块刷回主存中然后将当前数据进行写入另一种就是存在空闲的块此时就会将数据写入然后标记为脏数据。注意 LRU 策略下并不是将数据清除掉而是回检查当前数据块标志位是否为 dirty 如果是 dirty 就会将其刷入主存当中也就是说这种场景下我们进行写操作本身我们写入的是 i 的数据但是当前我们 LRU 需要去替换掉的是 g 的 cache line并且当前 cache line 的标记位本身为 dirty就会将 g 的数据先刷新到主存当中然后在写入 i 的数据这就是写回策略。
接下来看读策略
读策略相对来说更加容易理解CPU 在进行数据读取时首先会去缓存中读取如果命中就会直接读取到如果没有命中此时也是需要定位缓存块的如果定位到非脏的缓存块就直接从主存中将该数据刷入到缓存块当中标记为脏数据如果未命中同样的道理如果当前 cache line 标记位为脏数据会先将当前 cache line 的数据刷新会主存然后从主存中读取我们需要的数据到当前缓存中标记为非脏数据因为我们当前并没有写操作发生。 缓存不一致问题 了解了 CPU 读写数据的操作以后我们不难发现在多核多处理器下就会出现缓存不一致的问题试想一下有一个多核心的 CPU其中核心 A 和核心 B 都需要访问内存中的同一个数据 X 。一开始数据 X 被加载到核心 A 和核心 B 各自的缓存中 。当核心 A 对缓存中的数据 X 进行修改时此时核心 A 缓存中的数据 X 已经更新但是有 CPU 写回策略的存在核心 B 当中数据依然是旧的此时核心 B 进行读操作读取的就是旧的数据那么就会出现缓存不一致的问题。 基于写回策略的影响就出现了缓存一致性的问题在设计的过程中为了避免这种问题就出现了我们最开始所说的总线嗅探方案每个 CPU 核心都与总线相连接总线上一旦发生一些变化就会被 CPU 嗅探到当一个CPU核心对自己缓存中的数据进行写操作时它会向总线发送一个写请求这个请求包含了被修改数据的地址等信息 。
但是这种方式的问题就在于写操作是以广播的方式发送出去的每个核心都可以收到有一些核心并不需要当前的数据他也会收到这样就会造成带宽的压力所以在这儿也进行了优化确保我们当前的修改只有需要的核心会去访问到不需要的完全可以不用去管这样就减少了带宽的压力。
当然在这种总线嗅探机制下就会存在数据接收快慢的问题因为数据的传输是存在顺序的有的核心离得近有的核心离得远我们必须保证当前的数据修改被每一个需要核心所接收到并且在这个过程中数据并不会被更改在这块儿就会存在一个 lock 指令让其基于嗅探机制实现事务的串行化保证每一个核心所接收到的都是最新的数据。 缓存一致性协议 MESI MESI 协议是一个基于失效的缓存一致性协议支持 write-back 写回缓存的常用协议。
主要原理通过总线嗅探策略将读写请求通过总线广播给所有核心核心根据本地状态进行响应。
MESI 协议存在四种状态
ModifiedM某数据已修改但是没有同步到内存中。如果其他核心要读该数据需要将该数据从缓存同步到内存中并将状态转为 ExclusiveE某数据只在该核心当中此时缓存和内存中的数据一致SharedS某数据在多个核心中此时缓存和内存中的数据一致InvaliddateI某数据在该核心中以失效不是最新数据。
通过以上的知识介绍我们就可以理解原子性以及原子变量的原理有一个初步的了解接下来我们来看一下关于原子变量的一些接口调用
原子变量相关接口
store(T desired, std::memory_order order) 用于将指定的值存储到原子对象中load(std::memory_order order)用于获取原子变量的当前值exchange(std::atomicT* obj, T desired) 访问和修改包含的值将包含的值替换并返回它前面的值。如果替换成功则返回原来的值。compare_exchange_weak(T expected, T val, memory_order success, memory_order failure) 比较一个值和一个期望值是否相等如果相等则将该值替换成一个新值并返回 true否则不做任何操作并返回 false。注意compare_exchange_weak 函数是一个弱化版本的原子操作函数因为在某些平台上它可能会失败并重试。如果需要保证严格的原子性则应该使用 compare_exchange_strong 函数。fetch_addfetch_subfetch_andfetch_orfetch_xor这都是一些基于运算的操作。
这些函数仔细观察都会存在一个特点我们会发现存在一个 mem_order 这个代码的就是内存序内存序定义了原子操作之间的可见性关系和顺序约束直接影响程序的线程安全性。
内存序
我们平时的代码都点我们的逻辑顺序去写的但是对于 CPU 和编译器来说是会对代码进行优化的存在指令重排比说下面这段代码
int main()
{int i 0;int j 1;i 1;j 2;i 3;return 0;
}对于 CPU 和编译器来说他是可以不按照我们的代码逻辑来进行加载的比如说上面对于 i 的操作就会加载在一起然后先操作然后再加载对 j 的操作这也是 CPU 的局部性原理所决定的这样的优化可以去提高程序的运行效率。
在多线程的环境下CPU 的切换就会破坏逻辑我们会进行加锁操作而我们的加锁操作就是在控制内存序让 CPU 和编译器不再去进行优化这也就能解释为什么频繁的锁操作会影响程序的运行效率因为这样并不会让 CPU 和编译器对程序进行优化。
内存序规定了多个线程访问同一个地址时的语义他决定了某个线程对内存的操作何时能被其他线程所看见某个线程对内存附近的访问可以做到怎样的优化。
C 标准定义了 6 中内存序 memory_order_relaxed memory_order_relaxed 所代表是松散内存序只用来保证对原子对象的操作是原子的在不需要保证顺序时使用这也就意味着他只保证当前的操作是原子的对于代码逻辑随便 CPU 和编译器怎么去优化都可以我只要保证在我操作时其他线程不会操作就可以了就如下图所示 他就是值当前操作前面的代码是可以优化到后面去的后面的代码也是可以优化到前面去的但是当前不确定性就增加了效率却是最高的。
#include atomic
#include thread
#include iostreamstd::atomicint x{0};void thread_func1()
{for (int i 0; i 100000; i){x.store(i, std::memory_order_relaxed);}
}void thread_func2()
{for (int i 0; i 100000; i){x.store(-i, std::memory_order_relaxed);}
}int main()
{std::thread t1(thread_func1);std::thread t2(thread_func2);t1.join();t2.join();std::cout Final value of x x.load(std::memory_order_relaxed) std::endl;return 0;
}这段代码我们就可以确定结果要么就是 -9999 要么就是 9999 不会存在其他的值只是线程的调度顺序而已因为当前操作是具有原子性的。 memory_order_release memory_order_release 代表的是一个释放操作在写入某原子对象时当前线程的任何前面的读写操作都不允许重排到这个操作的后面去并且当前线程的所有内存写入都在对同一个原子对象进行获取的其他线程可见通常与 memory_order_acquire 或memory_order_consume 配对使用。 也就是说当前操作前面的操作可以任意优化后面的操作也可以优化到前面来但是当前操作前面的操作是不可以优化到后面去的而且当前操作完成以后数据同时会被刷新到 cache 和主存当中去其他线程所读取的就会是最新的数据。 memory_order_acquire memory_order_acquire 是一种获得操作在读取某原子对象时当前线程的任何后面的读写操作都不允许重排到这个操作的前面去并且其他线程在对同一个原子对象释放之前的所有内存写入都在当前线程可见。 也就是说当前操作前面的操作是可以优化到后面去的但是当前操作后面的操作不可以优化到前面去我们会先从主存当中去读取数据读值缓存当中我们每一次读取到的数据都是最新的通常与 memory_order_release 一起使用。
#include atomic
#include thread
#include assert.h
#include iostreamstd::atomicbool x,y;
std::atomicint z;// 提升效率
void write_x_then_y()
{x.store(true,std::memory_order_relaxed); // 1 y.store(true,std::memory_order_release); // 2
}void read_y_then_x()
{while(!y.load(std::memory_order_acquire)); // 3 自旋等待y被设置为trueif(x.load(std::memory_order_relaxed)) // 4z; // 会不会一定等于 1
}int main()
{xfalse;yfalse;z0;std::thread a(write_x_then_y);std::thread b(read_y_then_x);a.join();b.join();std::cout z.load(std::memory_order_relaxed) std::endl;return 0;
}我们看上面这段代码打印的结果一定是 1 原因就在于 3 操作必须等 2 操作完成以后才会结束对于 3 操作来说不会允许后面的操作被优化到她的前面而对于 2 操作来说不会允许它前面的操作被优化到后面去也就是说完成 2 操作以后1 操作必定被完成了最终只可能是 1 。 memory_order_acq_rel memory_order_acq_rel 是一个获得释放操作一个读‐修改‐写操作同时具有获得语义和释放语义即它前后的任何读写操作都不允许重排并且其他线程在对同一个原子对象释放之前的所有内存写入都在当前线程可见当前线程的所有内存写入都在对同一个原子对象进行获取的其他线程可见。 memory_order_seq_cst 顺序一致性语义对于读操作相当于获得对于写操作相当于释放对于读‐修改‐写操作相当于获得释放是所有原子操作的默认内存序并且会对所有使用此模型的原子操作建立一个全局顺序保证了多个原子变量的操作在所有线程里观察到的操作顺序相同当然它是最慢的同步模型。
shared_ptr的实现
在我们的 C11 中有很多地方会用到 shared_ptr shared_ptr 很好的解决了我们内存泄漏的问题它使用的是 RAII 的方法又引入了引用计数的概念支持多个对象去管理一个资源引用计数控制着当前资源被多少个对象所持有某个对象被销毁时引用计数就进行-- 操作当引用计数为 0 时就意味着该资源需要被释放了。
shared_ptr 我们也是可以通过原子变量去进行实现的在 shared_ptr 的内部存在对引用计数的 和 --操作就必定需要保证线程安全的问题就需要加锁但是使用原子变量的话可以是更优选相对于加锁来说原子变量的效率更高。
我们要实现一个 shared_ptr 就需要实现以下接口
构造函数析构函数拷贝构造函数拷贝赋值运算符移动构造函数解引用箭头运算符引用计数、原始指针、重置指针。
#ifndef __SHARED_PTR__
#define __SHARED_PTR__#include iostream
#include atomictemplate typename T
class Shared_ptr
{
public:// 智能指针是支持构造一个空对象的Shared_ptr() : ptr_(nullptr), count_(nullptr) {}// 构造函数explicit修饰防止被直接赋值explicit Shared_ptr(T *ptr) : ptr_(ptr), count_(ptr ? new std::atomicstd::size_t(1) : nullptr){}// 析构函数~Shared_ptr(){release();}// 拷贝构造函数Shared_ptr(const Shared_ptrT other) : ptr_(other.ptr_), count_(other.count_){if (count_){count_-fetch_add(1);}}// 赋值运算符重载, 需要注意防止自己给自己拷贝Shared_ptrT operator(const Shared_ptrT other){if (this ! other){release();ptr_ other.ptr_;count_ other.count_;if (count_){count_-fetch_add(1);}}return this;}// 移动构造函数// 使用 noexcept 修饰代表当前函数不会存在异常编译器会生成高效代码Shared_ptr(const Shared_ptrT other) noexcept : ptr_(other.ptr_), count_(other.count_){other.ptr_ nullptr;other.count_ nullptr;}// 移动运算符重载Shared_ptrT operator(const Shared_ptrT other) noexcept{if (this ! other){release();ptr_ other.ptr_;count_ other.count_;other.ptr_ nullptr;other.count_ nullptr;}return this;}// 解引用T operator*() const{return *ptr_;}// -T *operator-() const{return ptr;}// 获取到引用计数size_t usecount() const{return count_ ? count_-load() : 0;}// 获取裸指针T *get() const{return ptr_;}// 重置函数void reset(T *ptr nullptr){release();ptr_ ptr;count_ ptr ? new std::atomicstd::size_t(1) : 0;}private:void release(){if (count_ count_-fetch_sub(1) 1){delete ptr_;delete count_;}}private:std::atomicint *count_; // 引用计数T *ptr_;
};#endif关于智能指针的原理可以参考之前的一篇文章C11之智能指针这儿只是换了另一种方式去进行实现没有考虑定制删除器与弱引用的问题同样上面的操作也没有对内存序进行设置默认就是 memory_order_seq_cst接下来我们来进行一下优化。
代码中使用 fetch_add 的地方有两处这两处位置使用 memory_order_relaxed 最为合适因为我们只需要保证这个操作是原子性的即可保证最终的值不会被影响而 fetch_sub 使用 memory_order_acq_rel 最为合适因为我们需要保证当前返回的是操作的原始值我们必须保证所有操作不被重排维持我们原始的代码逻辑而且我们也需要保证我们的变量是在为 0 时才进行释放的。
所以最终代码优化如下
#ifndef __SHARED_PTR__
#define __SHARED_PTR__#include iostream
#include atomictemplate typename T
class Shared_ptr
{
public:// 智能指针是支持构造一个空对象的Shared_ptr() : ptr_(nullptr), count_(nullptr) {}// 构造函数explicit修饰防止被直接赋值explicit Shared_ptr(T *ptr) : ptr_(ptr), count_(ptr ? new std::atomicstd::size_t(1) : nullptr){}// 析构函数~Shared_ptr(){release();}// 拷贝构造函数Shared_ptr(const Shared_ptrT other) : ptr_(other.ptr_), count_(other.count_){if (count_){count_-fetch_add(1, std::memory_order_relaxed);}}// 赋值运算符重载, 需要注意防止自己给自己拷贝Shared_ptrT operator(const Shared_ptrT other){if (this ! other){release();ptr_ other.ptr_;count_ other.count_;if (count_){count_-fetch_add(1, std::memory_order_relaxed);}}return *this;}// 移动构造函数// 使用 noexcept 修饰代表当前函数不会存在异常编译器会生成高效代码Shared_ptrT(const Shared_ptrT other) noexcept : ptr_(other.ptr_), count_(other.count_){other.ptr_ nullptr;other.count_ nullptr;}// 移动运算符重载Shared_ptrT operator(const Shared_ptrT other) noexcept{if (this ! other){release();ptr_ other.ptr_;count_ other.count_;other.ptr_ nullptr;other.count_ nullptr;}return *this;}// 解引用T operator*() const{return *ptr_;}// -T *operator-() const{return ptr_;}// 获取到引用计数size_t usecount() const{return count_ ? count_-load(std::memory_order_acquire) : 0;}// 获取裸指针T *get() const{return ptr_;}// 重置函数void reset(T *ptr nullptr){release();ptr_ ptr;count_ ptr ? new std::atomicstd::size_t(1) : 0;}private:void release(){if (count_ count_-fetch_sub(1, std::memory_order_acq_rel) 1){delete ptr_;delete count_;}}private:std::atomicstd::size_t *count_; // 引用计数T *ptr_;
};#endif测试是否是线程安全的
#include iostream
#include shared_ptr.h
#include thread
#include vector
#include chrono
#include memoryvoid test_shared_ptr_thread_safety() {Shared_ptrint ptr(new int(42));// 创建多个线程每个线程都增加和减少引用计数const int num_threads 10;std::vectorstd::thread threads;for (int i 0; i num_threads; i) {threads.emplace_back([ptr]() {for (int j 0; j 1000; j) {Shared_ptrint local_ptr(ptr);// 短暂暂停增加线程切换的可能性std::this_thread::sleep_for(std::chrono::milliseconds(1));}});}// 等待所有线程完成for (auto thread : threads) {thread.join();}// 检查引用计数是否正确std::cout use_count: ptr.usecount() std::endl;if (ptr.usecount() 1) {std::cout Test passed: shared_ptr is thread-safe! std::endl;} else {std::cout Test failed: shared_ptr is not thread-safe! std::endl;}
}// 测试代码
int main() {Shared_ptrint ptr1(new int(10));std::cout ptr1 use_count: ptr1.usecount() std::endl; // 1{Shared_ptrint ptr2 ptr1;std::cout ptr1 use_count: ptr1.usecount() std::endl; // 2std::cout ptr2 use_count: ptr2.usecount() std::endl; // 2}std::cout ptr1 use_count: ptr1.usecount() std::endl; // 1Shared_ptrint ptr3(new int(20));ptr1 ptr3;std::cout ptr1 use_count: ptr1.usecount() std::endl; // 2std::cout ptr3 use_count: ptr3.usecount() std::endl; // 2ptr1.reset();std::cout ptr1 use_count: ptr1.usecount() std::endl; // 0std::cout ptr3 use_count: ptr3.usecount() std::endl; // 1test_shared_ptr_thread_safety();return 0;
}
最终结果如下