计算机的乱序执行
- 一定会按正常顺序执行的情况
- 对同一块内存进行访问:如果代码对同一个内存地址进行操作,编译器和处理器通常保证这些操作的顺序性,以防止数据不一致。
- 变量依赖性:如果一个变量的值依赖于之前的变量,这些变量的定义和使用顺序通常不会被编译器改变,以确保程序逻辑的正确性。
- 其他情况可能进行乱序执行
- 在单线程环境下,乱序执行通常不会引起问题,但在多线程环境下,可能导致数据竞争和不一致。
C++中的内存模型
C++为了处理多线程中的内存一致性问题,提供了几种内存序(memory_order):
-
memory_order_relaxed
:最宽松的内存顺序。不保证任何执行顺序,只保证操作原子性。不同线程看到操作顺序可能不同。当我们讨论“不同线程看到操作顺序可能不同”这一概念时,我们在指的是多线程环境中可能出现的“内存不一致性”问题。这种现象是由于每个线程可能运行在不同的处理器上,每个处理器可能有自己的缓存,使得对共享变量的更新不是立即对所有线程可见的情况。这就可能导致不同线程看到共享数据更新的顺序不一致。
#include <atomic> #include <thread> #include <iostream> std::atomic<int> cnt = {0}; void increment() { for (int i = 0; i < 1000; ++i) { cnt.fetch_add(1, std::memory_order_relaxed); } } int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Count: " << cnt << '\n'; // 不保证输出2000,因为没有同步 return 0; }
-
memory_order_consume
:用于保证在原子操作之后,(只有)直接依赖于该原子变量的后续操作不会被排到该原子操作之前。std::atomic<int*> ptr; int v; void producer() { int* p = new int(42); v = 1024; ptr.store(p, std::memory_order_release); } void consumer() { int* p; while (!(p = ptr.load(std::memory_order_consume))); assert(*p == 42); // 总是成立 assert(v == 1024); // 可能不为1024,因为v的读取不是直接依赖于p }
-
memory_order_acquire
:用于获取释放的资源。确保在此原子加载操作之后的所有读写操作不会被重排到该原子操作之前。std::atomic<bool> ready = false; int data; void producer() { data = 42; // 数据准备 ready.store(true, std::memory_order_release); // 发布数据 } void consumer() { while (!ready.load(std::memory_order_acquire)); // 等待数据 assert(data == 42); // 数据可见 }
-
memory_order_release
:用于防止原子变量存储操作之前的写操作被重排到该原子存储操作之后。 -
memory_order_acq_rel
:结合了acquire和release,用于读-修改-写操作,确保操作前后的顺序性。- 在此原子操作之前的所有写操作(由同一线程执行)都不会被重排到该操作之后(release 语义)。
- 此原子操作之后的读取或写入(由同一线程执行)不会被重排到该操作之前(acquire 语义)。
std::atomic<int> count = 0; void modify() { int fetched = count.fetch_add(1, std::memory_order_acq_rel); // 读-修改-写 std::cout << "Count updated to " << fetched + 1 << '\n'; }
-
memory_order_seq_cst
:这是最严格的内存顺序,也是唯一一个能保证所有线程看到完全相同的操作顺序的内存顺序选项。这种内存顺序模仿了在单线程环境下的行为,使得编写的多线程程序在逻辑上更容易理解和预测。- 所有线程看到完全相同的操作顺序。
- 执行 memory_order_seq_cst 操作的所有线程就像按某一特定的全局顺序执行这些操作一样。
std::atomic<int> value(0); void write_thread() { value.store(3, std::memory_order_seq_cst); } void read_thread() { std::cout << "Value: " << value.load(std::memory_order_seq_cst) << '\n'; // 总是输出最新值 } int main() { std::thread writer(write_thread); std::thread reader(read_thread); writer.join(); reader.join(); return 0; }
内存屏障
-
什么是内存屏障(Memory Barrier)?
内存屏障(也称为内存栅栏)是一种同步机制,用来控制指令的执行顺序和保证内存操作的可见性及顺序。内存屏障阻止特定类型的操作(如加载和存储)在程序执行中被重新排序。这是在多处理器系统中维护一致性和同步非常关键的工具。
-
类型。内存屏障大致可以分为四类:
全屏障(Full Barrier):阻止所有前面的读写操作被重排到屏障之后,以及所有后面的读写操作被重排到屏障之前。
读屏障(Read Barrier):阻止所有前面的读操作被重排到屏障之后,确保之前的读操作在屏障后的读操作开始前完成。
写屏障(Write Barrier):阻止所有前面的写操作被重排到屏障之后,确保之前的写操作在屏障后的写操作开始前完成。
读写屏障(Read-Write Barrier):结合读屏障和写屏障的功能。 -
内存屏障的用途包括:
- 保证指令的执行顺序:确保编译器和处理器不会将指令重新排序到不符合程序员预期的顺序。
- 保证数据的可见性:确保修改后的数据能够及时被其他线程看到,防止线程间数据不一致的问题。
-
使用内存模型实现内存屏障
在 C++11 及以上版本中,内存模型的引入通过原子操作的内存顺序参数间接提供了内存屏障的功能。例如:memory_order_seq_cst
:行为类似于全屏障,确保全局操作的顺序性。memory_order_acquire
:在加载操作时,作用类似于读屏障。memory_order_release
:在存储操作时,作用类似于写屏障(只有此内存模型能实现屏障效果)。memory_order_acq_rel
:在读-修改-写操作中,结合获取和释放语义,类似于读写屏障。
-
GCC 提供了
__sync_synchronize()
,MSVC 提供了_ReadWriteBarrier()
等内存屏障函数。此外,直接使用汇编语言插入特定的内存屏障指令(如 x86 的MFENCE
、LFENCE
和SFENCE
)也是常见的做法。
副作用编程
- 无副作用编程:函数仅依赖输入参数,并在函数内部处理所有事情,不影响外部状态。
- 有副作用编程:函数影响或依赖外部状态,如修改全局变量或类的成员变量。
- 多线程下的副作用:一个线程的操作影响另一个线程的执行状态,需要同步机制来保证一致性。
信号量
- binary_semaphore:二进制信号量,只有两个状态,用于事件同步。
- counting_semaphore:计数信号量,允许多个线程同时访问资源。