C++ 多线程入门
进程与线程的区别
进程就是运行中的程序。
线程就是进程中的进程。
thread
创建线程
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
函数所在的线程)已结束而该线程仍在运行,该线程会被强制终止,不会继续执行。
悬空引用
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;
}
互斥量
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
操作。
互斥量死锁
看这个例子
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。两个线程互相等待,就形成了死锁。
lock_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);
unique_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
是支持移动语义。
std::call_once
单例模式是一种创建型设计模式,其核心目标是确保一个类仅有一个实例,并提供全局访问点
核心特点
-
唯一实例性
通过私有构造函数和静态成员变量,严格限制类只能生成一个实例
-
自行创建与初始化
类的实例由自身在首次调用时创建(懒汉式)或类加载时创建(饿汉式)
-
全局访问入口
通过静态方法提供统一的访问入口,确保所有代码操作同一实例
这是一个简单的例子
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 关键字,让类态成员变量类内初始化
};
condition_variable
的核心机制
生产者-消费者模型
首先我们先了解一个模型
- 生产者:生成数据并放入共享缓冲区,若缓冲区已满则等待。
- 消费者:从缓冲区取出数据,若缓冲区为空则等待。
- 同步目标:生产者不会覆盖未消费的数据,消费者不会读取无效数据。
condition_variable
- 等待(Wait):线程通过
condition_variable::wait
进入阻塞状态,直到被其他线程唤醒。等待时,线程会释放关联的互斥锁,允许其他线程获取锁。 - 通知(Notify):通过
condition_variable::notify_one
或notify_all
唤醒等待的线程。前者唤醒一个线程,后者唤醒所有等待线程。 - 条件检查:线程被唤醒后需重新检查条件(通常使用循环),防止虚假唤醒(即无明确通知下的意外唤醒)。
在下面的例子中,有一个生产者和两个消费者
#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;
}
异步并发
异步并发是一种结合异步执行和并发处理的编程范式,旨在高效管理多个任务,尤其在处理I/O密集型操作时优化资源利用和响应速度。
- 异步(Asynchronous)
指任务发起后不等待结果,继续执行后续代码,待任务完成后再通过回调、事件通知等方式处理结果。例如,发起网络请求后,程序可继续执行其他逻辑,无需阻塞等待响应。 - 并发(Concurrency)
指在同一时间段内处理多个任务的能力。并发可通过单线程的“任务切换”(如事件循环)或多线程/多核并行实现。例如,在单核CPU中,多个任务看似同时运行,实则是快速切换执行。
std::future
- 功能:用于获取异步操作的结果,是异步任务与主线程之间的桥梁。
- 关键方法
get()
阻塞直到结果就绪,并返回结果(仅可调用一次)。wait()
仅等待结果就绪,不获取值。wait_for()
/wait_until()
:超时等待结果。
- 特性:若异步任务抛出异常,
get()
会重新抛出该异常;future
不可复制,但支持移动语义。
std::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;
}
std::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_task
或std::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
的关系
async
≈ thread
+ packaged_task
std::async
内部封装了线程创建和任务执行逻辑,返回的future
可直接获取结果,无需显式管理线程。
为什么说约等于?
async
不仅支持异步执行,还支持延迟执行std::launch::deferred
。async
不需要显示的管理线程。
std::promise
类模板 std::promise
提供一种设施用以存储一个值或一个异常,之后通过 std::promise
对象所创建的 std::future
对象异步获得。
成员函数
set_value
设置结果为指定值set_value_at_thread_exit
设置结果为指定值,同时仅在线程退出时分发提醒set_exception
设置结果为指示异常set_exception_at_thread_exit
设置结果为指示异常,同时仅在线程退出时分发提醒
set_value
和 set_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
即可。
原子操作 std::atomic
C++ 中的原子操作(std::atomic
)是处理多线程并发编程的核心工具,用于确保共享数据的原子性、可见性和顺序一致性。以下是其关键要点:
- 原子性:原子操作是不可分割的单元,执行过程中不会被其他线程中断。例如,fetch_add() 保证对变量的增减操作是整体的,不会出现中间状态。
- 可见性:对原子变量的修改会立即对所有线程可见,避免因缓存不一致导致的数据不同步问题。
- 无锁编程:通过原子操作可以实现无锁数据结构(如自旋锁、计数器),减少锁竞争和上下文切换的开销。
我们来看这样的一个操作
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;
}
常用的成员函数
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^= |
与原子值进行逐位与、或、异或 |
内存序
C++ 定义了 6 种内存序选项,按约束强度从弱到强排列如下:
-
memory_order_relaxed
- 语义:仅保证原子性,不保证操作的顺序或可见性。
- 场景:适用于对顺序无要求的独立操作(如计数器累加)。
std::atomic<int> counter{0}; counter.fetch_add(1, std::memory_order_relaxed); // 其他线程可能不会立即看到结果
-
memory_order_consume
- 语义:确保后续依赖于当前操作的指令不会被重排到前面(依赖顺序)。
- 注意:实际中较少使用,通常被优化为
memory_order_acquire
-
memory_order_acquire
- 语义:用于读操作,保证当前线程的后续操作不会被重排到该读操作之前。
- 典型应用:与
memory_order_release
配对,实现“生产者-消费者”模型中的同步
// 消费者线程 while (data.load(std::memory_order_acquire) != 42) {} // 确保读取前生产者写入可见
-
memory_order_release
- 语义:用于写操作,保证当前线程的前序操作不会被重排到该写操作之后。
- 配对使用:与
memory_order_acquire
结合,确保数据发布到其他线程
// 生产者线程 data.store(42, std::memory_order_release); // 确保写入前所有操作已完成
-
memory_order_acq_rel
- 语义:结合
acquire
和release
,适用于读-修改-写操作(如compare_exchange_wea
)
- 语义:结合
-
memory_order_seq_cst
- 语义:默认选项,保证全局顺序一致性,所有线程看到的操作顺序一致。
- 代价:性能开销最大,适用于需要强一致性的场景(如银行交易)。