用内存顺序实现 三种内存顺序模型
文章目录
- C++ 内存顺序:用咖啡店工作模式理解并发编程
- 前言:为什么需要内存顺序?
- 三种内存顺序模型
- 1. 顺序一致性模型 (Sequential Consistency) - 像严格的工作流程
- 2. 宽松模型 (Relaxed Model) - 像自由工作的咖啡师
- 3. 获取-释放模型 (Acquire-Release) - 像有协调员的工作流程
- 解决单例模式的线程安全问题
- 有问题的单例模式(像没有标准配方的咖啡店)
- 安全的单例模式(像有标准流程的咖啡店)
- 综合示例:咖啡店运营模拟
- 总结:如何选择内存顺序
C++ 内存顺序:用咖啡店工作模式理解并发编程
前言:为什么需要内存顺序?
想象一下,你在一家繁忙的咖啡店工作。有多个咖啡师(线程)同时制作咖啡(处理数据),还有多个顾客(其他线程)等待他们的订单。如果没有明确的工作流程和沟通方式,可能会出现:
- 咖啡师不知道哪个订单应该先处理
- 顾客收到错误的咖啡
- 订单处理顺序混乱导致效率低下
C++ 内存顺序就是为了解决这类问题而设计的一套规则,确保在多线程环境中,各个线程能够有序、高效地协作。
三种内存顺序模型
1. 顺序一致性模型 (Sequential Consistency) - 像严格的工作流程
生活比喻:咖啡店有严格的标准操作流程,每个订单都必须按接收顺序处理,所有咖啡师看到的订单顺序完全一致。
#include <iostream>
#include <atomic>
#include <thread>
#include <cassert>// 咖啡订单状态
std::atomic<bool> order_taken(false);
std::atomic<bool> coffee_made(false);
int customer_satisfaction = 0;void barista() {// 等待接到订单while (!order_taken.load(std::memory_order_seq_cst)) {std::this_thread::yield(); // 等待订单}// 制作咖啡std::this_thread::sleep_for(std::chrono::milliseconds(100));coffee_made.store(true, std::memory_order_seq_cst);std::cout << "咖啡师: 咖啡制作完成!" << std::endl;
}void customer() {// 下订单order_taken.store(true, std::memory_order_seq_cst);std::cout << "顾客: 已下单,等待咖啡..." << std::endl;// 等待咖啡制作完成while (!coffee_made.load(std::memory_order_seq_cst)) {std::this_thread::yield(); // 等待咖啡}// 收到咖啡customer_satisfaction++;std::cout << "顾客: 收到咖啡,非常满意!" << std::endl;
}void test_seq_cst() {std::thread barista_thread(barista);std::thread customer_thread(customer);barista_thread.join();customer_thread.join();assert(customer_satisfaction == 1); // 确保顾客满意std::cout << "测试通过: 顺序一致性模型工作正常" << std::endl;
}
2. 宽松模型 (Relaxed Model) - 像自由工作的咖啡师
生活比喻:咖啡师可以自由决定处理订单的顺序,效率高但可能出现混乱。
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>std::atomic<int> total_coffees(0);void relaxed_barista(int id) {// 模拟制作咖啡时间std::this_thread::sleep_for(std::chrono::milliseconds(10 * (id + 1)));// 使用宽松内存顺序更新计数total_coffees.fetch_add(1, std::memory_order_relaxed);std::cout << "咖啡师 " << id << " 制作了一杯咖啡" << std::endl;
}void test_relaxed() {const int num_baristas = 5;std::vector<std::thread> baristas;// 启动多个咖啡师for (int i = 0; i < num_baristas; ++i) {baristas.emplace_back(relaxed_barista, i);}// 等待所有咖啡师完成for (auto& barista : baristas) {barista.join();}std::cout << "总共制作了 " << total_coffees.load() << " 杯咖啡" << std::endl;
}
3. 获取-释放模型 (Acquire-Release) - 像有协调员的工作流程
生活比喻:有一个协调员确保咖啡师在开始制作前能看到所有必要的订单信息。
#include <iostream>
#include <atomic>
#include <thread>
#include <cassert>
#include <string>std::atomic<std::string*> current_order(nullptr);
std::atomic<bool> order_ready(false);void order_taker() {// 接收订单std::string* order = new std::string("双倍浓缩拿铁");// 发布订单(释放语义)current_order.store(order, std::memory_order_release);std::cout << "订单接收员: 已接收订单 - " << *order << std::endl;
}void barista() {// 获取订单(获取语义)std::string* order;while ((order = current_order.load(std::memory_order_acquire)) == nullptr) {std::this_thread::yield();}std::cout << "咖啡师: 开始制作 - " << *order << std::endl;// 制作咖啡std::this_thread::sleep_for(std::chrono::milliseconds(150));// 标记订单完成(释放语义)order_ready.store(true, std::memory_order_release);std::cout << "咖啡师: 订单完成!" << std::endl;
}void customer() {// 等待订单完成(获取语义)while (!order_ready.load(std::memory_order_acquire)) {std::this_thread::yield();}std::string* order = current_order.load(std::memory_order_relaxed);std::cout << "顾客: 收到我的 " << *order << ",谢谢!" << std::endl;delete order; // 清理内存
}void test_acquire_release() {std::thread t1(order_taker);std::thread t2(barista);std::thread t3(customer);t1.join();t2.join();t3.join();std::cout << "测试通过: 获取-释放模型工作正常" << std::endl;
}
解决单例模式的线程安全问题
让我们用咖啡店的"唯一配方"来比喻单例模式,并解决其线程安全问题。
有问题的单例模式(像没有标准配方的咖啡店)
#include <iostream>
#include <memory>
#include <mutex>
#include <thread>class CoffeeRecipe {
private:CoffeeRecipe() {std::cout << "创建咖啡配方实例" << std::endl;}CoffeeRecipe(const CoffeeRecipe&) = delete;CoffeeRecipe& operator=(const CoffeeRecipe&) = delete;static std::shared_ptr<CoffeeRecipe> instance;static std::mutex instance_mutex;public:~CoffeeRecipe() {std::cout << "销毁咖啡配方实例" << std::endl;}static std::shared_ptr<CoffeeRecipe> getInstance() {// 第一次检查(没有内存屏障,可能看到部分构造的对象)if (!instance) {std::lock_guard<std::mutex> lock(instance_mutex);// 第二次检查if (!instance) {instance = std::shared_ptr<CoffeeRecipe>(new CoffeeRecipe);}}return instance;}void makeCoffee() const {std::cout << "使用秘制配方制作咖啡" << std::endl;}
};std::shared_ptr<CoffeeRecipe> CoffeeRecipe::instance = nullptr;
std::mutex CoffeeRecipe::instance_mutex;void test_unsafe_singleton() {std::thread t1([]() {auto recipe = CoffeeRecipe::getInstance();recipe->makeCoffee();});std::thread t2([]() {auto recipe = CoffeeRecipe::getInstance();recipe->makeCoffee();});t1.join();t2.join();
}
安全的单例模式(像有标准流程的咖啡店)
#include <iostream>
#include <memory>
#include <mutex>
#include <atomic>
#include <thread>class SafeCoffeeRecipe {
private:SafeCoffeeRecipe() {std::cout << "创建安全的咖啡配方实例" << std::endl;}SafeCoffeeRecipe(const SafeCoffeeRecipe&) = delete;SafeCoffeeRecipe& operator=(const SafeCoffeeRecipe&) = delete;static std::shared_ptr<SafeCoffeeRecipe> instance;static std::mutex instance_mutex;static std::atomic<bool> initialized;public:~SafeCoffeeRecipe() {std::cout << "销毁安全的咖啡配方实例" << std::endl;}static std::shared_ptr<SafeCoffeeRecipe> getInstance() {// 使用获取语义检查是否已初始化if (!initialized.load(std::memory_order_acquire)) {std::lock_guard<std::mutex> lock(instance_mutex);// 再次检查(使用宽松语义,因为已经在锁内)if (!initialized.load(std::memory_order_relaxed)) {instance = std::shared_ptr<SafeCoffeeRecipe>(new SafeCoffeeRecipe);// 使用释放语义标记已初始化initialized.store(true, std::memory_order_release);}}return instance;}void makeCoffee() const {std::cout << "使用安全秘制配方制作咖啡" << std::endl;}
};std::shared_ptr<SafeCoffeeRecipe> SafeCoffeeRecipe::instance = nullptr;
std::mutex SafeCoffeeRecipe::instance_mutex;
std::atomic<bool> SafeCoffeeRecipe::initialized = false;void test_safe_singleton() {std::thread t1([]() {auto recipe = SafeCoffeeRecipe::getInstance();recipe->makeCoffee();});std::thread t2([]() {auto recipe = SafeCoffeeRecipe::getInstance();recipe->makeCoffee();});t1.join();t2.join();
}
综合示例:咖啡店运营模拟
让我们创建一个完整的咖啡店模拟,展示不同内存顺序在实际场景中的应用。
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
#include <chrono>
#include <random>class CoffeeShop {
private:// 使用获取-释放模型协调订单处理std::atomic<int> pending_orders{0};std::atomic<int> completed_orders{0};// 使用宽松模型统计咖啡师工作量std::atomic<int> barista1_work{0};std::atomic<int> barista2_work{0};// 顺序一致性模型用于关键状态std::atomic<bool> shop_open{false};public:void open() {shop_open.store(true, std::memory_order_seq_cst);std::cout << "咖啡店开业了!" << std::endl;}void close() {shop_open.store(false, std::memory_order_seq_cst);std::cout << "咖啡店打烊了!" << std::endl;}void take_order(int customer_id) {// 模拟接收订单时间std::this_thread::sleep_for(std::chrono::milliseconds(10));// 发布新订单(释放语义)pending_orders.fetch_add(1, std::memory_order_release);std::cout << "顾客 " << customer_id << " 下了订单" << std::endl;}void prepare_order(int barista_id) {while (shop_open.load(std::memory_order_seq_cst)) {// 获取待处理订单(获取语义)int orders = pending_orders.load(std::memory_order_acquire);if (orders > 0) {// 尝试接单if (pending_orders.compare_exchange_strong(orders, orders - 1, std::memory_order_acq_rel)) {// 制作咖啡std::cout << "咖啡师 " << barista_id << " 开始制作咖啡" << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(50));// 完成订单(释放语义)completed_orders.fetch_add(1, std::memory_order_release);// 更新工作量统计(宽松语义)if (barista_id == 1) {barista1_work.fetch_add(1, std::memory_order_relaxed);} else {barista2_work.fetch_add(1, std::memory_order_relaxed);}std::cout << "咖啡师 " << barista_id << " 完成一杯咖啡" << std::endl;}} else {std::this_thread::yield();}}}void run() {open();// 启动咖啡师std::thread barista1(&CoffeeShop::prepare_order, this, 1);std::thread barista2(&CoffeeShop::prepare_order, this, 2);// 模拟顾客下单std::vector<std::thread> customers;for (int i = 0; i < 10; ++i) {customers.emplace_back(&CoffeeShop::take_order, this, i);}// 等待所有顾客下单for (auto& customer : customers) {customer.join();}// 等待所有订单完成while (completed_orders.load(std::memory_order_acquire) < 10) {std::this_thread::sleep_for(std::chrono::milliseconds(100));}close();barista1.join();barista2.join();// 输出统计信息std::cout << "今日营业结束:" << std::endl;std::cout << "咖啡师1制作了 " << barista1_work.load() << " 杯咖啡" << std::endl;std::cout << "咖啡师2制作了 " << barista2_work.load() << " 杯咖啡" << std::endl;std::cout << "总共完成了 " << completed_orders.load() << " 个订单" << std::endl;}
};int main() {CoffeeShop shop;shop.run();return 0;
}
总结:如何选择内存顺序
通过咖啡店的比喻,我们可以更好地理解不同内存顺序的适用场景:
-
顺序一致性 (seq_cst):像严格的标准操作流程,保证所有操作顺序一致,但性能开销最大。适用于需要强一致性的关键操作。
-
获取-释放 (acquire-release):像有协调员的工作流程,保证相关操作的顺序,性能适中。适用于需要同步但不要求全局顺序一致的场景。
-
宽松模型 (relaxed):像自由工作的咖啡师,只保证原子性,不保证顺序,性能最高。适用于统计、计数等不需要同步的场景。
在实际开发中,应该根据具体需求选择最合适的内存顺序,在保证正确性的前提下追求性能。就像经营咖啡店一样,需要在标准流程和灵活性之间找到平衡点。