在多线程编程中,对共享数据的并发访问需要特别注意顺序性和可见性。现代处理器和编译器为了提升性能,往往会对代码指令进行重排,这种重排可能会影响不同线程对共享数据的观察顺序,导致未定义行为。C++11引入了内存模型,其定义了一组以标准化多线程环境下的内存访问规则,控制多线程程序中数据的访问顺序和可见性,保证多线程程序中对共享数据的访问顺序与程序逻辑一致。避免数据竞争和未定义行为,进而保障数据的一致性和程序的正确性。
1. 内存序
内存模型的核心概念为内存序,通过内存序可以控制原子变量的读取和写入操作的顺序,从而保证多线程环境下的同步和顺序性。
C++11中的内存模型通过std::memory_order枚举定义了如下内存序:
enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
};
以上六种内存序分别代表不同的同步和顺序关系,用于控制多线程程序中数据的访问顺序。
1.1 Relaxed
memory_order_relaxed允许读写操作独立执行,不进行同步,是最松散的内存序。它只满足原子性而不关心可见性和顺序性的场景。代码示例:
#include <atomic>
std::atomic<int> x(0);
x.store(10, std::memory_order_relaxed); // 松散内存序
int y = x.load(std::memory_order_relaxed); // 松散内存序
1.2 Acquire
memory_order_acquire用于加载(load)共享数据,不可用于写数据——store函数。其保证当前线程中加载操作之后的任何读取和写入都不会被重排到加载操作之前;其他线程的所有release同一原子变量的写入操作为当前线程可见。适用于多线程的同步场景中确保前置依赖操作。代码示例:
#include <atomic>
std::atomic<int> flag(0);
int data;
if (flag.load(std::memory_order_acquire) == 1) {
// 获取内存序使得 data 可见
int read_data = data;
}
1.3 Release
memory_order_release用于写入(store)共享数据,不可用于读数据——load函数。其保证当前线程此store 之前的读写操作不会被重排到 store 之后,意味着在执行 store 操作之前,所有之前的写入操作在本线程中都是可见的;其他线程的所有acquire同一原子变量的 load 操作时保证这些写入的可见性。其适用于向其他线程发布数据。代码示例:
#include <atomic>
std::atomic<int> flag(0);
int data = 42;
data = 42;
flag.store(1, std::memory_order_release); // 发布 data
1.4 Acquire-Release
memory_order_acq_rel结合了Acquire和Release的特性,其应用于读——修改——写(Read-Modify-Write)操作,如fetch_add、fetch_sub、compare_excahnge等一个操作中既有读取又写入的情境下。它保证当前线程的读写操作不会重排到加载操作(load)之前存储操作(store)之后。其他线程的所有释放同一原子变量的写入为当前线程可见,该线程的所有写入操作可见于获得同一原子变量线程。适用于同时包含读取和写入的复杂同步场景。代码示例:
#include <atomic>
std::atomic<int> value(0);
value.fetch_add(1, std::memory_order_acq_rel); // 同时获取和释放
1.5 Consume
memory_order_consume用于读取共享数据,不可用于写数据——store函数。其保证当前线程中load操作之后的任何读取和写入都不会被重排到加载操作之前;其他线程的所有释放同一原子变量的写入为当前线程可见。通常只影响编译器优化,不常用。
1.6 Sequentially Consistent
memory_order_seq_cst是C++11中引入的最强内存序,其可同时应用于加载(load)和存储(store)操作,当其应用于加载load时等价于memory_order_acquire,其应用于存储store时等价于memory_order_release。当前线程的读写操作不能重排于加载操作之前,写入操作之后;其他线程的所有释放同一原子变量的写入为当前线程可见,该线程的所有写入操作可见于获得同一原子变量的其他线程。适用于高安全性需求的场景,但性能开销较大。代码示例:
#include <atomic>
std::atomic<int> counter(0);
counter.store(100, std::memory_order_seq_cst); // 严格一致的顺序
2. 内存模型的核心问题
内存模型的核心问题包括:可见性、顺序性和原子性。
3. 代码示例
以下代码示例展示了不同内存序的应用及其在实际场景中的效果,通过一个简单的示例说明了内存序在可见性和顺序性方面的影响。
3.1 Relaxed
以下代码展示了std::memory_order_relaxed内存序的效果。由于采用松散的内存序,在不同线程间,变量x和y的更新可能存在不一致性。
#include <atomic>
#include <thread>
#include <iostream>
std::atomic x(0), y(0);
void write_x() { x.store(1, std::memory_order_relaxed); }
void write_y() { y.store(1, std::memory_order_relaxed); }
void read_x_then_y()
{
while (x.load(std::memory_order_relaxed) != 1); // 等待 x 被写入
if (y.load(std::memory_order_relaxed) == 0)
{
std::cout << "y is 0\n"; // 该情况可能发生
}
}
int main()
{
std::thread t1(write_x);
std::thread t2(write_y);
std::thread t3(read_x_then_y);
t1.join();
t2.join();
t3.join();
}
在上述代码中,memory_order_relaxed允许变量x和y在不同线程间的可见性不一致,可能导致读线程read_x_then_y看到x更新但看不到y的更新(y的值仍未0)。
在MSVC2022中,确实出现了打印“y is 0”的情况
3.2 Acquire && Release
以下代码展示了使用memory_order_acquire和memory_order_release的生产者-消费者模型,通过这两种内存序确保线程间的顺序性和数据可见性。
#include <atomic>
#include <thread>
#include <iostream>
std::atomic data(0);
std::atomic ready(false);
void producer()
{
data.store(42, std::memory_order_relaxed); // 松散存储数据
ready.store(true, std::memory_order_release); // 发布 data
}
void consumer()
{
while (!ready.load(std::memory_order_acquire)); // 获取 ready 状态
std::cout << "Data: " << data.load(std::memory_order_relaxed) << "\n";
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
}
通过Release和Acquire内存序的组合,producer和consumer线程对于同一原子变量ready的读写操作使用了memory_order_acquire和memory_order_release,保证了consumer对ready的获取操后,可见data的写入。这种同步机制确保了线程间的可见性。
3.3 Sequentially Consistent
以下示例展示了顺序一致内存序的作用。在顺序一致性下,所有操作按全局顺序执行,确保了所有线程的观察顺序一致性。
#include <atomic>
#include <thread>
#include <iostream>
std::atomic counter(0);
void increment()
{
for (int i = 0; i < 100; ++i)
{
counter.fetch_add(1, std::memory_order_seq_cst);
}
}
int main()
{
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Counter: " << counter.load(std::memory_order_seq_cst) << "\n";
}
在此示例中,std::memory_order_seq_cst确保了所有线程对counter的操作按顺序执行,避免了重排问题,保证了线程间的数据一致性。所有线程都按严格的顺序对counter进行操作,从而得到期望的结果。
4. 总结
本文通过对C++内存模型及其内存序的详细解析,探讨了多线程环境下的可见性、顺序性和原子性问题。通过代码示例展示了不同内存序的实际应用效果,帮助开发者理解如何合理选择和使用内存序,以确保多线程程序的正确性和性能。正确理解并合理使用内存模型是保障多线程程序可靠性的关键。