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

C++ 内存序在多线程中的使用

目录

一、内存顺序

二、 指令重排在多线程中的问题  

2.1 问题与原因

2.2 解决方案 

 三、六种内存序

3.1 memory_order_relaxed

3.2  memory_order_consume

3.3 memory_order_acquire

 3.4 memory_order_release

3.5 memory_order_acq_rel

3.6 memory_order_seq_cst


一、内存顺序

        内存顺序是指在并发编程中, 对内存读写操作的执行顺序. 这个顺序可以被编译器和处理器进行优化, 可能会与代码中的顺序不同, 这被称为指令重排。

        如下代码,如果不处理器不能重拍两个加法的指令,则只能一行一行去执行;但是如果可以重拍指令,则可以在不同的处理单元中并行执行这两个加法操作,发挥处理器执行流水线的优势。

      

int x = 0, y = 1, a = 0, b = 1;void testMemoryOrder() {    a = b + 2;    x = y + 2;
}

二、 指令重排在多线程中的问题  

2.1 问题与原因

        在多线程中指令重排会引起一些问题,比如如下场景:

std::atomic<bool> ready{false};
std::atomic<int> data{0};

void producer() {    
	data.store(100, std::memory_order_relaxed); // 原子性的更新data的值, 但是不保证内存顺序 
	ready.store(true, std::memory_order_relaxed); // 原子性的更新ready的值, 但是不保证内存顺序
}

void consumer() {    // 原子性的读取ready的值, 但是不保证内存顺序    
	while (!ready.load(memory_order_relaxed)) 
	{          
		std::this_thread::yield(); //让出CPU时间片   
	}   
	// 当ready为true时, 再原子性的读取data的值    
	std::cout << data.load(memory_order_relaxed);  // 4. 消费者线程使用数据
}

int main() {       
	std::thread t1(producer);       
	std::thread t2(consumer);    
	t1.join();    
	t2.join();   
	return 0;
}

         我们预期的效果应该是当消费者看到ready为true时, 此时再去读取data的值。但实际的情况是, 消费者看到ready为true后, 读取到的data值可能仍然是0。

        一方面可能是指令重排引起的:在producer线程里, data和store是两个不相干的变量, 所以编译器或者处理器可能会将data.store(100, std::memory_order_relaxed);重排到ready.store(true, std::memory_order_relaxed);之后执行, 这样consumer线程就会先读取到ready为true, 但是data仍然是0。

       另一方面可能是内存顺序不一致引起的: 即使producer线程中的指令没有被重排, 但CPU的多级缓存会导致consumer线程看到的data值仍然是0。下面这张示意图来说明这个问题和CPU多级缓存的关系。

        每个CPU核心都有自己的L1 Cache与L2 Cache。producer线程修改了data和ready的值, 但修改的是L1 Cache中的值,producer线程和consumer线程的L1 Cache并不是共享的,所以consumer线程不一定能及时的看到producer线程修改的值。CPU Cache的同步是件很复杂的事情, 生产者更新了data和ready后,还需要根据MESI协议将值写回内存,并且同步更新其他CPU核心Cache里data和ready的值,这样才能确保每个CPU核心看到的data和ready的值是一致的。而data和ready同步到其他CPU Cache的顺序也是不固定的,可能先同步ready,再同步data, 这样的话consumer线程就会先看到ready为true, 但data还没来得及同步,所以看到的仍然是0。 

2.2 解决方案 

void producer() 
{   
	data.store(100, std::memory_order_relaxed); // 原子性的更新data的值, 但是不保证内存顺序   
	ready.store(true, std::memory_order_released); // 保证data的更新操作先于ready的更新操作
}

void consumer() 
{    
	// 保证先读取ready的值, 再读取data的值    
	 while (!ready.load(memory_order_acquire)) 
	 {           std::this_thread::yield(); 
	 }    
	 // 当ready为true时, 再原子性的读取data的值    
	 std::cout << data.load(memory_order_relaxed);
 }

  1. ready.store(true, std::memory_order_released):一方面限制ready之前的所有操作不得重排到ready之后,以保证先完成data的写操作, 再完成ready的写操作。 另一方面保证先完成data的内存同步, 再完成ready的内存同步,以保证consumer线程看到ready新值的时候,一定也能看到data的新值。

  2. ready.load(memory_order_acquire): 限制ready之后的所有操作不得重排到ready之前, 以保证先完成读ready操作,再完成data的读操作;

 三、六种内存序

        多线程程序中,为了保证程序的一致性和正确性,需要对内存操作的顺序进行控制。因为在现代处理器中,由于缓存一致性、乱序执行等优化,指令可能不会按顺序执行。内存序允许开发者显式地指定不同操作的顺序,以保证数据的一致性。      
        C++11 引入了 <atomic> 头文件,并定义了几种内存序类型,来控制原子操作的执行顺序,std::atomic提供了以下几个常用接口来实现原子性的读写操作:

// 原子性的写入值

std::atomic<T>::store(T val, memory_order sync = memory_order_seq_cst);

// 原子性的读取值

std::atomic<T>::load(memory_order sync = memory_order_seq_cst);

// 原子性的增加 counter.fetch_add(1)等价于++counter

std::atomic<T>::fetch_add(T val, memory_order sync = memory_order_seq_cst);

// 原子性的减少 counter.fetch_sub(1)等价于--counter

std::atomic<T>::fetch_sub(T val, memory_order sync = memory_order_seq_cst);

// 原子性的按位与 counter.fetch_and(1)等价于counter &= 1

std::atomic<T>::fetch_and(T val, memory_order sync = memory_order_seq_cst);

// 原子性的按位或 counter.fetch_or(1)等价于counter |= 1

std::atomic<T>::fetch_or(T val, memory_order sync = memory_order_seq_cst);

// 原子性的按位异或 counter.fetch_xor(1)等价于counter ^= 1

std::atomic<T>::fetch_xor(T val, memory_order sync = memory_order_seq_cst);

        memory_order用于指定内存顺序不同的内存序提供了不同的同步和顺序保证,从最弱到最严格依次如下 :

memory_order_relaxed(松散顺序)
memory_order_consume(消费顺序)
memory_order_acquire(获取顺序)
memory_order_release(释放顺序)
memory_order_acq_rel(获取-释放顺序)
memory_order_seq_cst(顺序一致性)

3.1 memory_order_relaxed

        基本概念:最宽松的内存序,它只保证操作的原子性,不涉及任何线程间的同步或顺序保证。这种方式下,编译器和CPU可以任意重排指令,但仍然保证操作是原子的。

        应用场景: 当只需要在多线程环境中执行简单的原子操作,而不需要与其他线程同步时,memory_order_relaxed 是最佳选择,典型场景是简单的计数器或统计类操作。 

std::atomic<int> counter(0);
//能保证原子性统计最终一致
void increment() {
    for (int i = 0; i < 100; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed); // 放松顺序
    }
}

3.2  memory_order_consume

        基本概念:消费顺序,用于确保在同一线程中,依赖于原子操作结果的读操作不会被重排到该原子操作之前。虽然设计上适用于生产者-消费者模型,但由于硬件优化,memory_order_consume 通常等同于 memory_order_acquire。

        应用场景:在多线程中,当一个线程生产数据,另一个线程消费数据并依赖这些数据时,可以使用 memory_order_consume。

std::atomic<int> p;

void producer() {
    p.store(100, std::memory_order_release); // 发布数据
}

void consumer() {
    int p = ptr.load(std::memory_order_consume); 
    std::cout << "Consumed: " << p << std::endl;
    
}

3.3 memory_order_acquire

        基本概念:获取顺序,确保在当前线程中,会在读操作之后插入一个LoadLoad屏障, 确保屏障之后的所有操作不会重排到屏障之前。这意味着,在线程读取数据后,它可以看到其他线程对共享变量的修改。

        使用场景:当你需要在读取数据时,确保前面的操作已经完成。

std::atomic<int> flag(0);
int data = 0;

void writer() {
    data = 100;
    flag.store(1, std::memory_order_release); // 先改data更新flag
}

void reader() {
    while (flag.load(std::memory_order_acquire) != 1); // 等待flag更新
    std::cout << "Data: " << data << std::endl; // 保证读取到最新的data值
}

 3.4 memory_order_release

        基本概念:释放顺序,确保在当前线程中,会在写操作之前插入一个StoreStore屏障, 确保屏障之前的所有操作不会重排到屏障之后。这意味着,其他线程在看到这个原子操作之后,也可以看到该线程之前的所有修改。

        使用场景:当你需要在更新共享数据时,确保在更新操作之前的写入已经完成。同上 flag.store(1, std::memory_order_release);保证执行到此处时,前面的data=100已经执行且对其他线程可见。

3.5 memory_order_acq_rel

        基本概念:获取-释放顺序,等效于memory_order_acquire和memory_order_release的组合,同时插入一个StoreStore屏障与LoadLoad屏障,确保当前线程中的操作既能看到其他线程的修改,又可以发布自己的修改。这种顺序适用于同时需要同步读写的场景。

        应用场景:当一个线程既要读取其他线程的状态,又要写入新的状态时,使用 memory_order_acq_rel。例如锁的实现。它的作用是确保 线程 B 在读取 x 时,所有之前对 x 的修改(或者其他操作)都已经完成,并且不会被延迟到 x 被读取之后,线程 B 之后的操作(比如打印 x 的值)不会在 x 读取之前执行。

std::atomic<int> x{0};

void threadA() {
    x.fetch_add(1, std::memory_order_acq_rel);  // 获取-释放操作
}

void threadB() {
    // 获取操作
    while (x.load(std::memory_order_acquire) == 0) 
    {  
     //等待交出时间片
    }
    std::cout << "Thread B can proceed";
}

3.6 memory_order_seq_cst

        基本概念:顺序一致性(Sequentially Consistent),保证所有线程看到的操作顺序是一致的,原子变量默认顺序。

        被memory_order_seq_cst标记的写操作,会立马将新值写回内存,而不仅仅只是写到Cache里就结束了;被memory_order_seq_cst标记的读操作,会立马从内存中读取新值,而不是直接从Cache里读取。这样相当于多个线程读写都在一个内存中,也就不存在Cache同步的顺序不一致问题。相比其他的memory_order,memory_order_seq_cst当于禁用了CPU Cache,会带来最大的性能开销了。

        使用场景:当需要确保所有线程对全局状态的顺序一致时,使用 memory_order_seq_cst。它适合那些需要绝对严格的同步场景。

相关文章:

  • 扫雷雷雷雷雷【水文勿三】
  • 能简述一下动态 SQL 的执行原理吗
  • 信号与系统第二章学习(六)
  • 利用Python爬虫按图搜索1688商品(拍立淘)
  • 安装好pycharm后,双击pycharm,出现“无法找到入口”,怎么办?
  • 第3章:启动界面与主界面设计
  • 解锁前端表单数据的秘密旅程:从后端到用户选择!✨
  • 线程池项目优化
  • mapbox高阶,结合threejs(threebox)实现立体三维飞线图
  • 时尚创意品牌海报徽标设计无衬线英文字体安装包 Scribles – A Brush Font
  • Python 面向对象高级编程-定制类
  • 8.RabbitMQ队列详解
  • mongodb安装教程以及mongodb的使用
  • springBoot01
  • 面试时,如何回答好“你是怎么测试接口的?”
  • 【玩转正则表达式】将正则表达式中的分组(group)与替换进行结合使用
  • 使用S8050三极管控制小风扇
  • K8s The connection to the server 192.168.56.120:6443 was refused报错解决
  • Linux纯命令行界面下SVN的简单使用教程
  • 超大规模分类(五):拍立淘图搜多模态解决方案
  • 企业做网站的意义/南宁网站建设网站推广
  • 做网站需要公司资质吗/百度问答官网
  • 网站的三级页面怎么做/seo视频教学网站
  • 网站部兼容是什么原因/拉新推广怎么找渠道
  • 小程序开发平台哪家公司好/seo综合诊断工具
  • 用ai怎么做网站/如何推广app让别人注册