内存序

内存序

1. 指令重排

指令重排可以是编译器的行为,也可以是处理器的行为.

  1. 编译器重排:为了优化程序性能,编译器在生成机器代码时可能会改变指令的顺序.例如,编译器可能会将两个没有数据依赖关系的指令交换顺序,或者将一个指令移动到一个条件分支外部.这种重排通常是安全的,因为编译器会确保重排后的程序与原程序在单线程环境中有相同的行为.

  2. 处理器重排:为了优化程序性能,现代处理器在执行指令时也可能会改变指令的顺序.例如,处理器可能会在一个指令等待数据时先执行后面的指令,或者将多个指令并行执行.这种重排也通常是安全的,因为处理器会确保重排后的程序与原程序在单核环境中有相同的行为.

然而,在多线程环境中,这两种重排都可能导致问题.因为一个线程看到的指令顺序可能与另一个线程看到的顺序不一致,这可能会导致数据竞争和其他并发问题.为了解决这个问题,我们需要使用内存屏障或原子操作来防止指令重排,确保内存操作的顺序.

2. 怎么避免指令重排导致多线程出现问题

简单来说,就是有操作内存顺序相关的指令,这些指令保证编译器和处理器都会按照设定顺序去运行,不会主动去优化和重排指令.

3. 什么是内存序

内存序(Memory Order)是一个计算机科学的概念,主要用于描述多处理器系统中内存操作的顺序。

在单处理器系统中,程序中的指令总是按照它们在程序中出现的顺序(即程序顺序)执行的。但是在多处理器系统中,由于各种优化技术(如指令重排和缓存),程序中的指令可能不会按照程序顺序执行。这就可能导致在一个处理器上看到的内存值和在另一个处理器上看到的内存值不一致,从而引发各种并发问题。

为了解决这个问题,计算机科学家引入了内存序的概念。内存序定义了内存操作(如读取和写入)在多处理器系统中的可见顺序。根据内存序的不同,我们可以将内存模型分为几种类型,如强内存模型(Strong Memory Model)和弱内存模型(Weak Memory Model)。

在强内存模型中,所有的内存操作都按照程序顺序执行。这种模型简单易懂,但是可能会限制系统的性能。

在弱内存模型中,内存操作可能不按照程序顺序执行。这种模型可以提高系统的性能,但是编程会更复杂,因为程序员需要显式地使用内存屏障(Memory Barrier)或原子操作(Atomic Operation)来保证内存操作的顺序。

3.1 内存序的一个例子

#include <iostream>
#include <thread>
#include <unistd.h>

int a = 0;
int b = 0;

void func_1() {
  a = 1; // 1
  std::this_thread::sleep_for(std::chrono::seconds(2));
  std::cout << b << std::endl; // 2
}

void func_2() {
  b = 2; // 3
  std::this_thread::sleep_for(std::chrono::seconds(2));
  std::cout << a << std::endl; // 4
}

int main() {
  std::thread t1(func_1);
  std::thread t2(func_2);
  t1.join();
  t2.join();
  return 0;
}
  • 使用sleep_for,是因为在大多数现代操作系统中,线程的时间片通常是几十毫秒到几百毫秒,所以在实际运行时,func_1 和 func_2 很可能会在同一个时间片内完成,也就是说,它们实际上是串行执行的。这就可能导致你看到的输出总是 “0,1” 或 “1,0”。

在C++中,std::memory_order是一个枚举类型,用于指定原子操作的内存顺序语义.这个枚举类型的值可以用于原子类型的成员函数,如std::atomic::load,std::atomic::store,std::atomic::exchange等.

std::memory_order有以下几个值:

  • std::memory_order_relaxed:不强制同步或顺序约束,只保证了原子操作本身不会被中断.
  • std::memory_order_consume:数据依赖性对于同一线程的后续读取操作强制排序.这意味着,如果一个操作A读取了一个原子变量,并且后续的操作B依赖于A的结果,那么A必须在B之前完成.
  • std::memory_order_acquire:在当前线程中,所有后续的读取操作必须在这个操作之后完成,防止读取操作被重排到这个操作之前.
  • std::memory_order_release:在当前线程中,所有先前的写入操作必须在这个操作之前完成,防止写入操作被重排到这个操作之后.
  • std::memory_order_acq_rel:同时具有acquire和release的语义,即在当前线程中,所有后续的读取操作必须在这个操作之后完成,所有先前的写入操作必须在这个操作之前完成.
  • std::memory_order_seq_cst:对于所有线程,所有的读取和写入操作都必须在这个操作之后完成,防止任何操作被重排到这个操作之前或之后.

这些内存顺序语义可以帮助你在多线程环境中正确地同步数据,避免数据竞争和其他并发问题.

4. 什么是内存屏蔽

内存屏障(Memory Barrier),也被称为内存栅栏,是一种同步原语,用于防止指令重排,确保特定的内存操作顺序.

在多核处理器系统中,为了提高性能,处理器和编译器可能会对指令进行重排.这种重排可能会导致在多线程环境中出现问题,因为一个线程看到的内存操作顺序可能与另一个线程看到的顺序不一致.

内存屏障可以防止这种情况的发生.它可以强制特定的内存操作顺序,例如:

  • Load Barrier(加载屏障):保证在屏障之前的所有加载操作,都在屏障之后的加载操作之前完成.
  • Store Barrier(存储屏障):保证在屏障之前的所有存储操作,都在屏障之后的存储操作之前完成.
  • Full Barrier(全屏障):同时包含加载屏障和存储屏障,保证在屏障之前的所有内存操作,都在屏障之后的内存操作之前完成.

在C++中,你可以使用std::atomic_thread_fence函数来创建一个内存屏障.例如,std::atomic_thread_fence(std::memory_order_acquire)会创建一个加载屏障,std::atomic_thread_fence(std::memory_order_release)会创建一个存储屏障,std::atomic_thread_fence(std::memory_order_seq_cst)会创建一个全屏障.