前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >原子变量——内存模型

原子变量——内存模型

作者头像
程序员的园
发布2024-11-08 16:12:50
220
发布2024-11-08 16:12:50
举报
文章被收录于专栏:程序员的园——原创文章

在多线程编程中,对共享数据的并发访问需要特别注意顺序性和可见性。现代处理器和编译器为了提升性能,往往会对代码指令进行重排,这种重排可能会影响不同线程对共享数据的观察顺序,导致未定义行为。C++11引入了内存模型,其定义了一组以标准化多线程环境下的内存访问规则,控制多线程程序中数据的访问顺序和可见性,保证多线程程序中对共享数据的访问顺序与程序逻辑一致。避免数据竞争和未定义行为,进而保障数据的一致性和程序的正确性。

1. 内存序

内存模型的核心概念为内存序,通过内存序可以控制原子变量的读取和写入操作的顺序,从而保证多线程环境下的同步和顺序性。

C++11中的内存模型通过std::memory_order枚举定义了如下内存序:

代码语言:javascript
复制
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允许读写操作独立执行,不进行同步,是最松散的内存序。它只满足原子性而不关心可见性和顺序性的场景。代码示例:

代码语言:javascript
复制
#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同一原子变量的写入操作为当前线程可见。适用于多线程的同步场景中确保前置依赖操作。代码示例:

代码语言:javascript
复制
#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 操作时保证这些写入的可见性。其适用于向其他线程发布数据。代码示例:

代码语言:javascript
复制
#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)之后。其他线程的所有释放同一原子变量的写入为当前线程可见,该线程的所有写入操作可见于获得同一原子变量线程。适用于同时包含读取和写入的复杂同步场景。代码示例:

代码语言:javascript
复制
#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。当前线程的读写操作不能重排于加载操作之前,写入操作之后;其他线程的所有释放同一原子变量的写入为当前线程可见,该线程的所有写入操作可见于获得同一原子变量的其他线程。适用于高安全性需求的场景,但性能开销较大。代码示例:

代码语言:javascript
复制
#include <atomic>
std::atomic<int> counter(0);
counter.store(100, std::memory_order_seq_cst); // 严格一致的顺序

2. 内存模型的核心问题

内存模型的核心问题包括:可见性、顺序性和原子性。

  • 可见性:在多线程环境中,线程对共享变量的修改并不一定能被其他线程立即可见。使用std::memory_order_acquire和std::memory_order_release内存序可以确保共享变量在不同线程间的可见性。例如,生产者线程通过memory_order_release发布某些共享数据,而消费者线程则通过memory_order_acquire读取这些数据,从而保证数据在线程间的可见性。
  • 顺序性:顺序性是指操作执行的顺序保证在不同线程间一致。C++内存模型中,std::memory_order_seq_cst(顺序一致性)提供了严格的顺序保证,所有线程都观察到相同的操作顺序,避免数据的重排问题。在多线程中,顺序性是保证逻辑一致的重要因素。
  • 原子性:原子性确保操作的不可分割性,即在多线程环境下,操作要么全部完成,要么全部不执行。C++内存模型中的所有原子操作都具备不可分割性,避免了在多线程环境中发生数据竞争的风险。原子操作的不可分割性为多线程编程提供了基础的线程安全保障。

3. 代码示例

以下代码示例展示了不同内存序的应用及其在实际场景中的效果,通过一个简单的示例说明了内存序在可见性和顺序性方面的影响。

3.1 Relaxed

以下代码展示了std::memory_order_relaxed内存序的效果。由于采用松散的内存序,在不同线程间,变量x和y的更新可能存在不一致性。

代码语言:javascript
复制
#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的生产者-消费者模型,通过这两种内存序确保线程间的顺序性和数据可见性。

代码语言:javascript
复制
#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

以下示例展示了顺序一致内存序的作用。在顺序一致性下,所有操作按全局顺序执行,确保了所有线程的观察顺序一致性。

代码语言:javascript
复制
#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++内存模型及其内存序的详细解析,探讨了多线程环境下的可见性、顺序性和原子性问题。通过代码示例展示了不同内存序的实际应用效果,帮助开发者理解如何合理选择和使用内存序,以确保多线程程序的正确性和性能。正确理解并合理使用内存模型是保障多线程程序可靠性的关键。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-11-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员的园 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档