您的位置:首页 > 汽车 > 时评 > 【一分钟学C++】std::memory_order

【一分钟学C++】std::memory_order

2024/11/17 14:43:22 来源:https://blog.csdn.net/rainInSunny/article/details/142266024  浏览:    关键词:【一分钟学C++】std::memory_order

在这里插入图片描述

竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生~
公众号: C++学习与探索  |  个人主页: rainInSunny  |  个人专栏: Learn OpenGL In Qt

文章目录

  • 写在前面
  • 为什么需要Memory Order
  • Memory Order
    • Relaxed Order
    • Release-Acquire Order

写在前面

  使用std::memory_order是用来限制编译器以及CPU对单线程当中的指令执行顺序进行重排的程度。这种限制,决定了以atom操作为基准点,对其之前的内存访问命令,以及之后的内存访问命令,能够在多大的范围内自由重排,从而形成了6种模式。这里我们主要讨论std::memory_order_relaxedstd::memory_order_acquirestd::memory_order_release。 注意,std::memory_order限制的是单线程中的CPU指令乱序,但一般用来解决多线程同步的问题。

为什么需要Memory Order

  如果不使用任何同步机制(例如mutex或atomic),在多线程中读写同一个变量,程序的结果是难以预料的。简单来说,编译器以及CPU的一些行为,会影响到程序的执行结果:

  • 即使是简单的语句,C++也不保证是原子操作。
  • CPU可能会调整指令的执行顺序。
  • 在CPU cache的影响下,一个CPU执行了某个指令,不会立即被其它CPU看见。
// 场景1:C++不保证线程2输出的是100,因为i = 100不是原子操作,可能存在中间态
int i = 0;
Thread_1:
i = 100;Thread_2:
std::cout << i;// 场景2:CPU可能会调整指令的执行顺序,这里假设所有操作都是原子操作,仍然可能输出0或者100
int x = 0;
int y = 0;Thread_1:
x = 100;
y = 200;Thread_2:
while (y != 200);
std::cout << x;// 场景3:假设A先于B,但CPU cache的影响下,Thread_2不能保证立即看到A操作的结果,所以Thread_2可能输出0或100
int x = 0;Thread_1:
x = 100; // AThread_2:
std::cout << x; // B

  场景1,i = 100;不是原子操作导致了结果不确定;场景2,CPU会在不影响当前线程执行逻辑情况下对指令执行顺序进行优化,如果Thread_1将y = 200调整到x = 100之前执行,那么可能输出0或者100;场景3,由于CPU缓存,可能导致Thread_2在输出的时候Thread_1中x的值还不可见,导致可能输出0或者100。

typedef enum memory_order {memory_order_relaxed, // relaxedmemory_order_consume, // consumememory_order_acquire, // acquirememory_order_release, // releasememory_order_acq_rel, // acquire/releasememory_order_seq_cst  // sequentially consistent
} memory_order;

  可以看出多线程读写变量需要同步机制,常见的有std::mutexstd::atomic,对比两者std::atomic性能要更好一些。C++标准库提供了std::memory_orderstd::atomic一起实现多线程之间同步,下面主要讲解std::memory_order_relaxedstd::memory_order_acquirestd::memory_order_release

Memory Order

Relaxed Order

std::atomic<int> x = 0;
std::atomic<int> y = 0;Thread_1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // BThread_2:
r2 = x.load(memory_order_relaxed); // C
y.store(66, memory_order_relaxed); // D

  执行完上面的程序,可能出现r1 == r2 == 66。理解这一点并不难,因为编译器允许调整C和D的执行顺序。如果程序的执行顺序是D -> A -> B -> C,那么就会出现r1 == r2 == 66。当只需要保证原子性,不需要其它同步操作时,选择使用std::memory_order_relaxed

Release-Acquire Order

  在这种模型下,store()使用std::memory_order_release,而load()使用std::memory_order_acquire。这种模型有两种效果,第一种是可以限制CPU指令的重排。除此之外,还有另一种效果:假设 Thread_1中store()的那个值成功被Thread_2中load()到了,那么Thread_1在store()之前对内存的所有写入操作,此时对Thread_2来说都是可见的。

  • 在store()之前的所有内存读写操作,不允许被移动到这个store()的后面。
  • 在load()之后的所有内存读写操作,不允许被移动到这个load()的前面。
#include <thread>
#include <atomic>
#include <cassert>
#include <string>std::atomic<bool> ready{ false };
int data = 0;
void producer()
{data = 66; // Aready.store(true, std::memory_order_release); // B
}
void consumer()
{while (!ready.load(std::memory_order_acquire)) // C;assert(data == 66); // D,never failed
}
int main()
{std::thread t1(producer);std::thread t2(consumer);t1.join();t2.join();return 0;
}

  由于在store()之前的所有内存读写操作,不允许被移动到这个store()的后面,所以线程t1中A操作一定在B操作之前就执行完毕了。线程t2中C操作跳出循环时意味着线程t1中B操作执行完毕,那么data此时肯定已经被赋值为66,所以assert永远不会失败。反之如果这里使用std::memory_order_relaxed,那么t1线程中data的赋值可能被CPU调整到store()后面,那就可能导致assert失败。

在这里插入图片描述


关注公众号:C++学习与探索,有惊喜哦~

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com