ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C/C++——atomic小记

C/C++——atomic小记 CAS操作CAS操作就是“compare_and_swap”比较和交换。这是实现atomic原子操作的基石在一些平台下CAS本身就是一个原子指令因此是原子的在另一些平台下CAS是硬件模拟原子的不止由一个指令组成。CAS的weak和strongCAS在C中有两种接口compare_exchange_weak 和 compare_exchange_strong 一般我们说weak是不安全但是高效的、strong是安全但是不效的。只有在多指令CAS下才有weak和strong这个说法如果是单指令CAS不会有这种区别。在多指令CAS中操作分为三步第一步LL (Load-Linked) - 读取值并标记这个内存地址 LL R1, [内存地址] ; R1 100开始监视这个地址 第二步比较 CMP R1, 期望值 ; 100 100是的 第三步SC (Store-Conditional) - 条件存储 SC R2, [内存地址] ; 尝试写入新值 ; 但如果这个地址被打扰过写入失败 ; 返回 0失败看懂了吗也就是说在第一步而第三步中间可能有其他线程会直接修改变量值因为缓存一致性对于本线程来说这个变量的寄存器值失效了所以无法写入所以执行失败。这种判断相等之后依旧执行失败的现象叫做“伪失败”正常失败是在第一步的时候比较失败这是正常的但是伪失败是在我们已经赋予CAS原子性语义的情况下中间操作失败这是逻辑上的错误所以说如果我们直接使用compare_exchange_weak可能会出现伪失败最终导致数据不一致的情况。但是这是可以避免的// weak 版本 - 可能伪失败 while (!value.compare_exchange_weak(expected, newvalue)) { // 如果失败包括伪失败expected 会被更新为当前值 // 循环重试 }而compare_exchange_strong做了什么呢// strong 的内部实现伪代码 bool compare_exchange_strong(expected, desired) { // 内部循环调用 weak 版本 while (true) { bool success compare_exchange_weak(expected, desired); if (success) { return true; // 成功了 } // 失败有两种情况 if (expected ! actual_value) { return false; // 真失败值确实变了 } // 伪失败值没变继续循环重试 // expected 已经被更新为当前值还是原值 // 继续下一次尝试 } }实际上strong也是循环重试而compare_exchange_strong和compare_exchange_weak的性能差异就体现在多出来的两个条件判断上。strong为了维护CAS原子性的语义不能把两种失败都直接返回false因此在其内部做了判断区分。atomic无锁头插链表typedef struct node { node(int val,node* ne) { _val val; _next ne; } struct node* _next; int _val 0; }Node; atomicNode* head(nullptr); void func(int i) { Node* old head.load(); Node* newnode new Node(i,nullptr); do { newnode-_next old; } while (!head.compare_exchange_weak(old, newnode)); } int main() { int k 0; vectorthread arr; for (int i 0;i10;i) { arr.push_back(thread(func, k)); } for ( auto e:arr) { e.join(); } Node* cur head.load(); while(cur!nullptr) { cout cur-_val ; cur cur-_next; } cout endl; }atomic内存序一些时候为了优化程序效率会把指令顺序重排。对于指令乱序这块需要注意的是编译器只需要保证在单线程环境下执行的结果最终一致就可以了所以指令乱序在单线程环境下完全是允许的。但是编译器并不知道哪些数据是在线程间共享而且是有可能会被修改的这些是需要开发人员去保证的。而atomic这个类天生就是为多线程而生因此一旦你使用atomic那么就需要考虑内存序问题内存序就是限制本来可以重排的命令之间的顺序以保证多线程并发安全锁在内部其实也保证了内存序只是它封装起来了我们看不到atomic的操作比如说load比如说compare_exchange_weak都可以设置内存序以防止出错。1.memory_order_relaxed最宽松语义仅保证原子操作的原子性不提供任何同步或顺序约束重排限制无编译器和CPU可任意重排该操作周围的指令示例std::atomicint x(0); x.store(42, std::memory_order_relaxed); // 仅保证原⼦性2.memory_order_acquire获取语义语义这个内存序只可以在包含读操作的atomic操作中使用比如load比如exchange读操作之后可能本线程就要能执行了这个内存序保证读操作之后的所有读写操作都一定是在可以执行之后才执行的不能因为重排而打乱导致并发问题。重排限制防止后续操作读或写被提到该操作之前执行3.memory_order_release释放语义语义这个内存序只可以在包含写操作的atomic操作中使用比如storeexchange写操作之后其他线程就会被唤醒这个内存序保证写操作之前的所有读写操作都一定是被执行的防止被唤醒的线程访问出错。重排限制防止先前操作读或写被延迟到该操作之后执行示例​​​std::atomicbool flag(false); int data 0 // 线程 1 data 42; flag.store(true, std::memory_order_release); // 保证 data 42 在 flagtrue 之前可⻅ // 线程 2 while (!flag.load(std::memory_order_acquire)) {} std::cout data; // 保证看到 data 42上面是一个线程修改另一个线程负责打印的例子如果没有 std::memory_order_release 那么有可能flag的设置会在data赋值之前这样可能会造成线程2打印出0的错误结果。如果没有 std::memory_order_acquire 编译器可能会认为while循环和打印交换没什么影响因为在线程2的函数中data没有被修改因此出现错误。4.memory_order_acq_rel获取-释放混合语义同时具有acquire和release的语义一般用于同时进行读写操作的原子操作比如fetch_add。重排限制既防止前面的操作被移到之后也防止后面的操作被移到之前双向限制示例std::atomicint counter(0); int old counter.fetch_add(1, std::memory_order_acq_rel); // 既保证之前操作不会被重排到后面也保证之后操作不会被重排到前面5.memory_order_seq_cst顺序一致性默认语义所有线程看到的内存操作顺序完全一致全局顺序一致性重排限制最强限制完全禁止所有相关重排并提供全局同步点示例std::atomicint x(0); x.store(42, std::memory_order_seq_cst); // 默认模式 int value x.load(std::memory_order_seq_cst); // 所有线程保证看到相同顺序
返回列表