C++ 多线程入门

1|0进程与线程的区别


进程就是运行中的程序。

线程就是进程中的进程。

2|0thread


创建线程

void foo(int x) { cout << "thread id : " << std::this_thread::get_id() << "\n"; cout << "x = " << x << "\n"; } int main() { std::thread t1{foo, 1}; std::thread t2{foo, 2}; }

这里创建了两个线程,我们运行代码,发现输出格式是混乱的。这其实也反应了我们的线程是并行执行的。该如何解决呢?我们可以使用thread::join()函数,等待线程完成其执行。

int main() { std::thread t1{foo, 1}; t1.join(); std::thread t2{foo, 2}; t2.join(); }

这样后一个进程必须要等待前一个进程被执行完成才能创建。

由此可以引出一个函数thread::joinable(), 检查线程是否可合并,即潜在运行于并行上下文之中。

什么是可合并的线程?结束执行代码,但仍未合并的线程仍被当作活跃的执行线程,从而是可合并的。

因此其返回值就是:若 std::thread 对象标识活跃的执行线程则为 true,否则为 false。

注意,构造线程对象时,如果需要传引用,则需要std::ref

现在我们再来看这个例子

void foo(int x) { cout << "thread id : " << std::this_thread::get_id() << "\n"; cout << "x = " << x << "\n"; } int main() { std::thread t1{foo, 1}; return 0; }

这个例子只有一个线程。但是如果我们运行的话,会发现一个报错terminate called without an active exception。这是因为当线程被创建后,线程就会开始执行,同时主函数会继续执行。此时主函数就会return 0;,也就可能会发生主函数已经结束,但是输出还没有完成的情况。因此就会产生这个报错。对于这个问题当然可以用thread::join()解决。当这样话,代码就会变成顺序执行,也就失去了多线程的意义。

对于这种情况,我们可以用thread::detach(),从 thread 对象分离执行线程,允许它独立地持续执行。当该线程退出时将释放其分配的任何资源。调用 detach*this 不再占有任何线程。

注意当一个线程被detach后,若主线程(通常是main函数所在的线程)已结束而该线程仍在运行,该线程会被强制终止,不会继续执行

3|0悬空引用


void foo(int *x) { std::this_thread::sleep_for(std::chrono::seconds(2)); cout << (*x) << "\n"; } int main() { auto ptr = new int(123); std::thread t(foo, ptr); delete ptr; t.join(); return 0; }

这个例子,在绝大多数的情况下,都不会得到正确的结果。这是因为当需要输出(*x)时,ptr已经被delete了。此时x就变成了悬空指针。

对于这个问题,我们就可以用智能指针实现。

void foo(std::shared_ptr<int> x) { std::this_thread::sleep_for(std::chrono::seconds(2)); cout << (*x) << "\n"; } int main() { std::shared_ptr<int> ptr = std::make_shared<int>(123); std::thread t(foo, ptr); t.join(); return 0; }

再看下面的例子

void foo() { for (int i = 0; i < 1000; i++) cout << i; cout << "\nover\n"; } void call() { std::thread t(foo); return; } int main() { call(); return 0; }

这个例子会出现什么问题?可能出现的情况是call()函数已经结束运行了,但是线程t还没有结束。

对于这个问题,我们可以用RAII的思路解决。首先先实现一个类。

struct thread_guard { std::thread &_t; // 线程引用 explicit thread_guard(std::thread &_t) : _t(_t) {} ~thread_guard() { if (_t.joinable()) _t.join(); } // 删除拷贝构造和拷贝赋值 thread_guard(thread_guard const &) = delete; thread_guard &operator=(thread_guard const &) = delete; };

这个类引用了一个线程,这个类在析构时会保证_t已经执行完。

void call() { std::thread t(foo); thread_guard tg(t); return; }

4|0互斥量


void foo(int &x) { for (int i = 1; i <= 100000; i++) x += 1; } int main() { int x = 0; std::thread t1(foo, std::ref(x)); std::thread t2(foo, std::ref(x)); t1.join(); t2.join(); cout << x << "\n"; return 0; }

这个x的值应该是200000。但你运行的 结果,大概率不是。这是因为线程是可以并行执行的,可能会对数据进行竞争,也就是两个函数同时进行x += 1操作,也就会造成其中的一个操作无效。

为了避免数据竞争,我们可以通过互斥锁来实现这个。

std::mutex mtx; void foo(int &x) { for (int i = 1; i <= 100000; i++){ mtx.lock(); x += 1; mtx.unlock(); } }

只要这样,无论怎么运行结果都一定是200000。注意这里的锁并不是锁某个对象,而是锁lock()unlock()之间的操作。也就是两个线程不能同时执行x +=1 操作。

5|0互斥量死锁


看这个例子

std::mutex m1, m2; void f1() { for (int i = 0; i < 500; i++) { m1.lock(); cout << "f1 m1 lock\n"; m2.lock(); cout << "f1 m2 lock\n"; m1.unlock(); cout << "f1 m1 unlock\n"; m2.unlock(); cout << "f1 m2 unlock\n"; } } void f2() { for (int i = 0; i < 500; i++) { m2.lock(); cout << "f2 m2 lock\n"; m1.lock(); cout << "f2 m1 lock\n"; m1.unlock(); cout << "f2 m1 unlock\n"; m2.unlock(); cout << "f2 m2 unlock\n"; } } int main() { std::thread t1(f1); std::thread t2(f2); t1.join(); t2.join(); cout << "over" << "\n"; return 0; }

运行结果如下

f1 m1 lock f2 m2 lock

并且程序卡住了,为什么?因为线程t1在等待线程t2释放m2,线程t2在等待线程t1释放m1。两个线程互相等待,就形成了死锁。

6|0lock_guard


我们刚才看的例子,都需要手动进行上锁和解锁。这种做法其实不符合RAII思想。实际上C++内置了一个std::lock_guard,用法非常简单。当创建 lock_guard 对象时,它尝试接收给定互斥体的所有权。当控制离开创建 lock_guard 对象的作用域时,销毁 lock_guard 并释放互斥体。

std::mutex mtx; void foo(int &x) { for (int i = 0; i < 10000; i++) { std::lock_guard<std::mutex> lg(mtx); x += 1; } } int main() { int x; std::thread t(foo, std::ref(x)); return 0; }

我们可以看一下源码的实现

template<typename _Mutex> class lock_guard { public: typedef _Mutex mutex_type; explicit lock_guard(mutex_type &__m) : _M_device(__m) { _M_device.lock(); } lock_guard(mutex_type &__m, adopt_lock_t) noexcept: _M_device(__m) {} // calling thread owns mutex ~lock_guard() { _M_device.unlock(); } lock_guard(const lock_guard &) = delete; lock_guard &operator=(const lock_guard &) = delete; private: mutex_type &_M_device; };

这个实现和我们之前实现thread_guard非常相似,注意这里有第二个构造函数,这个构造函数并不会上锁,只会记录这个锁状态。如果需要用第二个构造函数可以

std::lock_guard<std::mutex> lg(mtx,std::adopt_lock);

7|0unique_lock


unique_lock 是一种通用互斥包装器,允许延迟锁定、有时限的锁定尝试、递归锁定、所有权转移和与条件变量一同使用。类 unique_lock 可移动,但不可复制。

自动上锁解锁比较简单。

void foo(int &x) { for (int i = 0; i < 10000; i++) { std::unique_lock<std::mutex> lg(mtx); x += 1; } }

如果需要延迟上锁

void foo(int &x) { for (int i = 0; i < 10000; i++) { std::unique_lock<std::mutex> lg(mtx, std::defer_lock); // do somthing ... lg.lock(); // 延迟加锁 x += 1; } }

我们来看有时限尝试锁定如何实现。首先这里我们不能使用std::mutex,而应该使用std::timed_mutex

void foo(int &x) { for (int i = 0; i < 10000; i++) { std::unique_lock<std::timed_mutex> lg(mtx, std::defer_lock); // 并不会自动上锁 if (lg.try_lock_for(std::chrono::seconds(2))) x += 1; } }

这个锁会尝试如果2秒内成功上锁就会返回true。否则,这个锁不会持续阻塞,返回一个false

除此之外,他还有一个成员函数是std::unique_lock<Mutex>::try_lock_until,他需要你传入一个时间点,这个时间点之前他会尝试锁定,超过时间点就终止阻塞。

unique_lock是支持移动语义。

8|0std::call_once


单例模式是一种创建型设计模式,其核心目标是确保一个类仅有一个实例,并提供全局访问点

核心特点

  1. 唯一实例性

    通过私有构造函数和静态成员变量,严格限制类只能生成一个实例

  2. 自行创建与初始化

    类的实例由自身在首次调用时创建(懒汉式)或类加载时创建(饿汉式)

  3. 全局访问入口

    通过静态方法提供统一的访问入口,确保所有代码操作同一实例

这是一个简单的例子

class Log { public: Log(const Log &other) = delete; Log &operator=(const Log &other) = delete; static Log &getInstance() { static Log *log = nullptr; if (log == nullptr) log = new Log; return *log; } void printLog(std::string msg) { std::cerr << __TIME__ << " " << msg << std::endl; } private: Log() = default; };

在这个例子中如果我需要打印日志。

Log::getInstance().printLog("Error");

好的,现在有一个问题是如果这个代码在多线程中运行,这个代码就会出现问题,比如if (log == nullptr) log = new Log;如果发生了数据竞争,这里就会多次构造。就没有保证实例唯一。这样我们可以用解决std::call_once解决。

template<class Callable, class... Args> void call_once(std::once_flag& flag, Callable&& f, Args&&... args);

我们来看call_once函数定义,首先需要有一个once_flag,还有一个可调用对象f,这样就能保证在多个线程中这个可调用对象只被执行一次。

static Log &getInstance() { static Log *_log = nullptr; std::once_flag _once; std::call_once(_once, [&]() { _log = new Log; // 因为只用执行一次,因此也就不需要判断了。 }); return *_log; }

除此之外,这个代码还有一些问题就是打印的时候没有上锁,可能会日志混乱,_log没有RAII规范等,之前的代码中实际上就会造成内存泄漏,因为我没有在析构函数中手动deleta _log。为此可以做出以下更改。

class Log { public: Log(const Log &other) = delete; Log &operator=(const Log &other) = delete; static Log &getInstance() { static Log _log; // 符合 RAII return _log; } void printLog(std::string _msg) { std::lock_guard<std::mutex> lg(_mtx); // 自动上锁 std::cerr << __TIME__ << " " << _msg << std::endl; } private: Log() = default; ~Log() = default; static inline std::mutex _mtx; // C++17 新特性,可以用 inline 关键字,让类态成员变量类内初始化 };

9|0condition_variable 的核心机制


9|1生产者-消费者模型


首先我们先了解一个模型

  • 生产者:生成数据并放入共享缓冲区,若缓冲区已满则等待。
  • 消费者:从缓冲区取出数据,若缓冲区为空则等待。
  • 同步目标:生产者不会覆盖未消费的数据,消费者不会读取无效数据。

9|2condition_variable


  1. 等待(Wait):线程通过condition_variable::wait进入阻塞状态,直到被其他线程唤醒。等待时,线程会释放关联的互斥锁,允许其他线程获取锁。
  2. 通知(Notify):通过condition_variable::notify_onenotify_all唤醒等待的线程。前者唤醒一个线程,后者唤醒所有等待线程。
  3. 条件检查:线程被唤醒后需重新检查条件(通常使用循环),防止虚假唤醒(即无明确通知下的意外唤醒)。

在下面的例子中,有一个生产者和两个消费者

#include <iostream> #include <thread> #include <functional> #include <algorithm> #include <random> #include <chrono> #include <memory> #include <mutex> #include <condition_variable> #include <queue> using std::cout; std::mt19937 rd(1); std::mutex mtx; std::condition_variable not_empty, not_full; const int MAX_SIZE = 5; std::queue<int> buffer; bool production_done = false; // 生产结束标志 void producer() { for (int i = 0; i < 30; i++) { std::unique_lock<std::mutex> lock(mtx); not_full.wait(lock, [] { return buffer.size() < MAX_SIZE; }); // 等待缓冲区不满 buffer.push(i); cout << "producer push " << i << "\n"; // 通知消费者缓冲区非空 not_empty.notify_one(); } std::unique_lock<std::mutex> lock(mtx); production_done = true; not_empty.notify_all(); } void consumer(std::string id) { while (true) { std::unique_lock<std::mutex> lock(mtx); // 等待缓冲区非空 not_empty.wait(lock, [] { return (not buffer.empty()) or production_done; }); if (production_done and buffer.empty()) break; std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟生产耗时 int data = buffer.front(); buffer.pop(); cout << "consumer_" << id << " pop " << data << "\n"; not_full.notify_one(); } } int main() { std::thread p{producer}; std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟生产耗时 std::thread c0{consumer, "0"}; std::thread c1{consumer, "1"}; p.join(); c0.join(); c1.join(); return 0; }

10|0异步并发


异步并发是一种结合异步执行并发处理的编程范式,旨在高效管理多个任务,尤其在处理I/O密集型操作时优化资源利用和响应速度。

  1. 异步(Asynchronous)
    指任务发起后不等待结果,继续执行后续代码,待任务完成后再通过回调、事件通知等方式处理结果。例如,发起网络请求后,程序可继续执行其他逻辑,无需阻塞等待响应。
  2. 并发(Concurrency)
    指在同一时间段内处理多个任务的能力。并发可通过单线程的“任务切换”(如事件循环)或多线程/多核并行实现。例如,在单核CPU中,多个任务看似同时运行,实则是快速切换执行。

10|1std::future


  • 功能:用于获取异步操作的结果,是异步任务与主线程之间的桥梁。
  • 关键方法
    • get()阻塞直到结果就绪,并返回结果(仅可调用一次)。
    • wait() 仅等待结果就绪,不获取值。
    • wait_for()/wait_until():超时等待结果。
  • 特性:若异步任务抛出异常,get()会重新抛出该异常;future不可复制,但支持移动语义。

10|2std::async


函数模板 std::async 异步地运行函数 (有可能在可能是线程池一部分的分离线程中),并返回最终将保有该函数调用结果的 std::future

有两种启动策略

  • std::launch::async:立即在新线程中执行。
  • std::launch::deferred:延迟到调用get()wait()时执行。

我们下面来看一个简单例子。

首先我定义了一个函数initRandomVector,并且随机化两个数组,现在我要求出两个数组和的绝对值,我可以写出一下的代码。

std::mt19937 rd(1); void initRandomVector(std::vector<int> &vec) { int n = rd() % 100; vec.resize(n); for (auto &i: vec) i = rd() % 100; return; } int main() { std::vector<int> a, b; initRandomVector(a), initRandomVector(b); auto calcSum = [](std::vector<int> vec) -> int { int sum = 0; for (const auto &i: vec) sum += i; return sum; }; int a_sum = calcSum(a); int b_sum = calcSum(b); cout << abs(b_sum - a_sum) << "\n"; return 0; }

然后我们注意到两个数组求和的过程实际上是独立的,也就是可以并行执行的,并且对于a_sum的值,也只有最后计算绝对值的时候才会用到。因此我们可以把计算a数组和的部分异步并发也就是先发起求和a的任务,然后不等待计算结果,继续执行对b求和。

这样的话代码可以优化成下面的样子

int main() { std::vector<int> a, b; initRandomVector(a), initRandomVector(b); auto calcSum = [](std::vector<int> vec) -> int { int sum = 0; for (const auto &i: vec) sum += i; return sum; }; std::future<int> a_sum = std::async(std::launch::async, calcSum, a); // 发起求和 a 的任务 int b_sum = calcSum(b); cout << abs(b_sum - a_sum.get()) << "\n"; return 0; }

10|3std::packaged_task


类模板 std::packaged_task 包装任何可调用目标(函数、lambda 表达式、bind 表达式或其他函数对象),使得能异步调用它。其返回值或所抛异常被存储于能通过 std::future 对象访问的共享状态中。

关键方法

  • operator():执行任务,结果自动写入future
  • get_future():获取关联的future对象。

注意:std::packaged_task必须显示的描述可调用函数的类型。

注意:std::packaged_task不支持复制,只支持移动。这是因为它内部封装了可调用对象和共享状态(用于存储异步结果),这些资源需要独占性管理,避免多个对象同时操作导致数据竞争或逻辑错误

void task_lambda() { std::packaged_task<int(int, int)> task([](int a, int b) -> int { // 封装任务 return a + b; }); std::future<int> result = task.get_future(); // 获取任务关联的 future 对象 task(2, 3); // 传入所需参数,并执行任务 cout << result.get() << "\n"; }

这里是异步执行的,我们创建任务后没有等待任务执行而是继续操作。这里不仅可以手动传入参数,也可以用bind表达式传入在构造时直接传入参数。

void task_bind() { std::packaged_task<int()> task(std::bind([](int a, int b) { return a + b; }, 3, 4));// 封装任务 auto result = task.get_future();// 获取任务关联的 future 对象 task(); // 并执行任务 cout << result.get() << "\n"; }

但是这样只能实现异步,如何实现并发?我们可以和thread结合使用。

int main() { std::vector<int> a, b; initRandomVector(a), initRandomVector(b); auto calcSum = [](const std::vector<int> &vec) -> int { int sum = 0; for (const auto &i: vec) sum += i; return sum; }; std::packaged_task<int(const std::vector<int> &)> task_calc(calcSum); // 封装任务 auto result_a = task_calc.get_future(); // 获取任务关联 future 对象,必须在创建线程之前。 std::thread thread_calc_a(std::move(task_calc), std::cref(a)); // 创建线程并传入参数,任务开始执行 int b_sum = calcSum(b); cout << std::abs(b_sum - result_a.get()) << "\n"; thread_calc_a.join(); // 确保线程终止。 return 0; }

这里有几个问题,我们逐一思考

为什么获取关联对象必须在创建线程之前?

因为创建线程中使用到了std::move,因此创建线程后task_calc对象变为空状态,因此无法获得关联对象。

为什么必须要使用std::move?

因为std::packaged_task不支持拷贝,只支持移动,需要std::move

std::thread接受可调用对象是Function&& f也就是转发引用(万能引用),为什么一定要std::move包装成右值引用,不能用std::ref包装成左值引用?

如果采用左值引用,可能会出现多个线程访问同一任务对象,引发未定义行为。比如如下代码可以正常编译运行,但是会出现运行时错误。

void test() { std::packaged_task<int(int, int)> task([](int a, int b) { return a + b; }); std::thread t1{std::ref(task), 2, 3}; std::thread t2{std::ref(task), 3, 4}; auto result = task.get_future(); cout << result.get() << "\n"; t1.join(); t2.join(); return; }

std::futurd::get()已经可以确保任务完成,为什么还需要std::thread::join()确保线程结束?

  • 阻塞等待结果get() 会阻塞当前线程,直到异步任务完成并将结果存储到共享状态中。例如,在 std::packaged_taskstd::async 的场景中,get() 会等待任务执行完毕并返回结果。
  • 不直接控制线程get() 仅保证异步任务的逻辑执行完毕(结果已就绪),但不保证线程本身已终止。例如,如果线程中还有其他代码在 std::packaged_task 执行之后运行,这些代码可能未被 get() 等待。

比如下述例子中,任务已结束,但是线程没有结束。

void test() { auto longTask = []() { std::this_thread::sleep_for(std::chrono::seconds(3)); return 1919; }; auto keepRunning = []() { int cnt{}; while (true) { std::this_thread::sleep_for(std::chrono::seconds(1)); cout << ++cnt << "\n"; } }; std::packaged_task<int()> task(longTask); std::thread t([&] { task(); keepRunning(); }); auto result = task.get_future(); cout << result.get() << "\n"; return; }

因此为了更方便的管理进程,可以用std::async实现。

std::async的关系

asyncthread + packaged_task

std::async内部封装了线程创建和任务执行逻辑,返回的future可直接获取结果,无需显式管理线程。

为什么说约等于?

  1. async不仅支持异步执行,还支持延迟执行std::launch::deferred
  2. async不需要显示的管理线程。

10|4std::promise


类模板 std::promise 提供一种设施用以存储一个值或一个异常,之后通过 std::promise 对象所创建的 std::future 对象异步获得。

成员函数

  • set_value 设置结果为指定值
  • set_value_at_thread_exit 设置结果为指定值,同时仅在线程退出时分发提醒
  • set_exception 设置结果为指示异常
  • set_exception_at_thread_exit 设置结果为指示异常,同时仅在线程退出时分发提醒

set_valueset_value_at_thread_exit区别

  • set_value 立即获得共享状态的值,并同步将状态标记修改为ready。此时任何阻塞在 future::get()future::wait() 的线程会立即被唤醒并获取结果
  • set_value_at_thread_exit,设置共享状态的值,但延迟标记状态为 ready,直到当前线程退出且所有线程局部对象被销毁后,共享状态才变为 ready

异步并发的简单例子

void test() { auto add = [](std::promise<int> &res, int x, int y) { res.set_value(x + y); }; std::promise<int> prom; auto res = prom.get_future(); std::thread t{add, std::ref(prom), 3, 5}; cout << res.get() << "\n"; t.join(); return; }

注意,promise不只用于异步并发,实际上可以用于线程间传递信息。让生产者获得promise,消费者获得对应的future即可。

11|0原子操作 std::atomic


C++ 中的原子操作(std::atomic)是处理多线程并发编程的核心工具,用于确保共享数据的原子性、可见性和顺序一致性。以下是其关键要点:

  1. 原子性:原子操作是不可分割的单元,执行过程中不会被其他线程中断。例如,fetch_add() 保证对变量的增减操作是整体的,不会出现中间状态。
  2. 可见性:对原子变量的修改会立即对所有线程可见,避免因缓存不一致导致的数据不同步问题。
  3. 无锁编程:通过原子操作可以实现无锁数据结构(如自旋锁、计数器),减少锁竞争和上下文切换的开销。

我们来看这样的一个操作

void solve1() { int x = 0; std::mutex mtx; auto f = [&](){ for(int i = 0; i < 100000; i ++){ std::unique_lock<std::mutex> lg(mtx); x ++; } }; std::thread t1(f); std::thread t2(f); t1.join(), t2.join(); cout << x << endl; return; }

这里上锁是因为有多个线程可能会同时访问一个变量。我们可以用原子操作来省去加锁的步骤,因为原子操作不会被其他进程终端。

void solve2() { std::atomic<int> x(0); // 初始化 auto f = [&](){ for(int i = 0; i < 100000; i ++) x ++; }; std::thread t1(f); std::thread t2(f); t1.join(), t2.join(); cout << x << endl; return; }

11|1常用的成员函数


load()

原子读取,可以std::atomic<T>::load()读取T的值。

我们上面的操作cout << x << endl;是隐式的调用了load()。也就是

cout << x.load() << endl;

store()

原子写入:将值写入 std::atomic 对象,确保操作是原子的,不会被其他线程的读写操作干扰。

std::atomic<int> x(0); x = 132; x.store(132);

上面赋值实际上就是隐式的调用了store()函数。

为整数、浮点数(C++20起)、指针的特化成员函数

fetch_add原子地将实参加到存储于原子对象的值上

fetch_sub原子地从存储于原子对象的值减去实参

特性 fetch_add/fetch_sub operator+=/operator-=
返回值 操作前的旧值 操作后的新值
底层实现 原子性“读取-修改-写入”操作 通常基于fetch_add/fetch_sub实现
适用场景 需要获取旧值(如计数器、CAS循环) 仅需更新值(简洁语法)
内存序 可显式指定(如std::memory_order_relaxed 默认std::memory_order_seq_cst

为整数和指针类型特化

opetator++,operator-- 令原子值增加或减少一

仅为整数类型特化

fetch_and 原子地进行实参和原子对象的值的逐位与,并获得先前保有的值 (公开成员函数)
fetch_or 原子地进行实参和原子对象的值的逐位或,并获得先前保有的值 (公开成员函数)
fetch_xor 原子地进行实参和原子对象的值的逐位异或,并获得先前保有的值 (公开成员函数)
operator&=,operator|=,operator^= 与原子值进行逐位与、或、异或

11|2内存序


C++ 定义了 6 种内存序选项,按约束强度从弱到强排列如下:

  1. memory_order_relaxed

    • 语义:仅保证原子性,不保证操作的顺序或可见性。
    • 场景:适用于对顺序无要求的独立操作(如计数器累加)。
    std::atomic<int> counter{0}; counter.fetch_add(1, std::memory_order_relaxed); // 其他线程可能不会立即看到结果
  2. memory_order_consume

    • 语义:确保后续依赖于当前操作的指令不会被重排到前面(依赖顺序)。
    • 注意:实际中较少使用,通常被优化为memory_order_acquire
  3. memory_order_acquire

    • 语义:用于读操作,保证当前线程的后续操作不会被重排到该读操作之前。
    • 典型应用:与memory_order_release配对,实现“生产者-消费者”模型中的同步
    // 消费者线程 while (data.load(std::memory_order_acquire) != 42) {} // 确保读取前生产者写入可见
  4. memory_order_release

    • 语义:用于写操作,保证当前线程的前序操作不会被重排到该写操作之后。
    • 配对使用:与memory_order_acquire结合,确保数据发布到其他线程
    // 生产者线程 data.store(42, std::memory_order_release); // 确保写入前所有操作已完成
  5. memory_order_acq_rel

    • 语义:结合acquirerelease,适用于读-修改-写操作(如 compare_exchange_wea)
  6. memory_order_seq_cst

    • 语义:默认选项,保证全局顺序一致性,所有线程看到的操作顺序一致。
    • 代价:性能开销最大,适用于需要强一致性的场景(如银行交易)。

__EOF__

本文作者PHarr
本文链接https://www.cnblogs.com/PHarr/p/18749703.html
关于博主:前OIer,SMUer
版权声明CC BY-NC 4.0
声援博主:如果这篇文章对您有帮助,不妨给我点个赞
posted @   PHarr  阅读(23)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示