【C++现代#2】C++11 并发支持库 -- 互斥量(Mutex)及锁机制 (<mutex>)

📃个人主页:island1314
⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
- 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》
🔥 目录
互斥量(Mutex)及锁机制 ()
互斥量(Mutex)用于保护共享数据,防止多个线程同时访问造成数据竞争 (Data Race)。
| 组件 | 描述 |
|---|---|
std::mutex |
基本互斥量: 提供排他性所有权,用于保护临界区。通常通过调用 lock() 和 unlock() 来手动控制,或使用下面的 RAII 封装。 |
std::lock_guard |
推荐: 基于 RAII (Resource Acquisition Is Initialization) 机制的锁封装。在构造时锁定互斥量,在析构时(即离开作用域时)自动解锁。简单且异常安全。 |
std::unique_lock |
也是 RAII 锁封装,但更灵活。它支持延迟锁定、定时锁定,并且可以手动控制解锁/加锁,以及在函数间转移所有权(可移动)。 |
std::recursive_mutex |
允许同一个线程多次锁定它而不会导致死锁。一般不推荐使用,除非确实需 |
📌 关键点: 为了保证代码的异常安全,强烈推荐 使用
std::lock_guard或std::unique_lock而不是直接手动调用std::mutex::lock()和std::mutex::unlock()。
1. mutex
背景:现在我们先来看看如下一段代码
int gtx = 0;
mutex mtx;
void Print(int n) {
for (int i = 0; i < n; ++i) {
++gtx;
}
}
int main() {
thread t1(Print, 100000);
thread t2(Print, 200000);
t1.join();
t2.join();
cout << gtx << endl;
return 0;
}
但是 这里的 全局变量 gtx 并不是想象的 300000,而是比这个值更小。原因:++gtx 在机器层面被拆成 “读-改-写” 三步,两个线程可以同时读到同一中间值,于是其中一次更新被丢失——这就是典型的 数据竞争(data race)。
假设当前 gtx == 10,线程 1 和线程 2 几乎同时到达 ++gtx:
| 时间 | 线程 1(CPU-1) | 线程 2(CPU-2) | 说明 |
|---|---|---|---|
| t0 | r1 = gtx // 10 |
把 10 读到寄存器 r1 | |
| t1 | r2 = gtx // 10 |
线程 2 也读到 10 | |
| t2 | r1 = r1 + 1 // 11 |
寄存器加 1 | |
| t3 | r2 = r2 + 1 // 11 |
线程 2 也加 1 | |
| t4 | gtx = r1 // 11 |
写回内存,gtx 变成 11 | |
| t5 | gtx = r2 // 11 |
再次写回 11,把线程 1 的结果覆盖掉 |
两条线程各自累加了 1,但内存最终只增加了 1 而不是 2;
当这个过程在 10 万次循环里随机发生,总增量必然远小于 300 000。
补充:操作系统/硬件视角
- 即使没有编译器优化,
++gtx也只是 单条 C 语句,不是 单条机器指令。 - 在多核缓存体系下,“读-改-写” 三步可能跨越多个时钟周期,并且不会被总线自动串行化。
- 因此必须靠程序员显式加锁(或原子指令) 才能保证“读-改-写”整体不可打断。
解决如下:让“读-改-写”成为临界区
void Print(int n) {
mtx.lock(); // 进入互斥区
for (int i = 0; i < n; ++i) {
++gtx;
}
mtx.unlock(); // 离开互斥区
}
1.1 基本概述
- 定义:
std::mutex类在<mutex>头文件中定义。它是 C++ 标准库提供的基本互斥量类型。 - 作用: 解决数据竞争 (Data Race) 问题。当多个线程同时访问和修改同一块内存数据时,如果不加保护,最终结果将是不可预测的,这就是数据竞争。
mutex通过“加锁”机制来防止这种情况发生。 - 特性:
std::mutex提供排他性 (Exclusive) 和非递归性 (Non-recursive) 的所有权语义。这意味着:- 一旦一个线程锁定了
mutex,其他试图锁定它的线程都会被阻塞(直到锁被释放)。 - 锁定它的线程不能在释放之前再次锁定它(非递归),否则会导致 未定义行为 或 死锁。
- 一旦一个线程锁定了
std::mutex 本身提供三个核心的成员函数来控制锁:
| 函数 | 作用 | 描述 |
|---|---|---|
lock() |
加锁 | 锁定互斥量。如果互斥量已经被其他线程锁定,则当前线程会阻塞,直到互斥量被释放。 |
unlock() |
解锁 | 释放互斥量。只有拥有该互斥量的线程才能调用 unlock()。释放后,等待中的线程中有一个会被唤醒去竞争锁。 |
try_lock() |
尝试加锁 | 尝试锁定互斥量,但不阻塞。如果成功锁定则返回 true,否则立即返回 false。适用于需要避免阻塞,或尝试获取多个锁的场景。 |
示例:try_lock 使用
std::timed_mutex mtx;
void fireworks(int i){
// std::cout << i;
// waiting to get a lock: each thread prints "-" every 200ms:
while (!mtx.try_lock_for(std::chrono::milliseconds(1000))){
std::cout << "-";
}
std::cout << i;
// got a lock! - wait for 1s, then this thread prints "*"
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
std::cout << "*\n";
mtx.unlock();
}
int main(){
std::thread threads[2];
// 利⽤移动赋值的⽅式,将创建的临时对象(右值对象)移动赋值给创建好的空线程对象
for (int i = 0; i < 2; ++i) threads[i] = std::thread(fireworks, i);
for (auto& th : threads) th.join();
return 0;
}
补充:time_mutex 和 recursive_mutex
time_mutex跟mutex完全类似,只是额外提供try_lock_for和try_lock_untile的接口,这两个接口跟try_lock类似,只是他不会马上返回,而是直接进入阻塞,直到时间条件到了或者解锁了就会唤醒试图获取锁资源。recursive_mutex跟mutex完全类似,recursive_mutex提供 排他性递归所有权语义:- 调用方线程在从它成功调用 lock 或 trylock开始的时期里占有
recursive_mutex。此时期之内,线程可以进行对lock或try_lock的附加调用。所有权的时期在线程进行匹配次数的 unlock 调用时结束。 - 线程占有
recursive_mutex时,若其他所有线程试图要求recursive_mutex的所有权则它们将阻塞(对于调用 lock)或收到 false 返回值(对于调用 try _lock)
- 调用方线程在从它成功调用 lock 或 trylock开始的时期里占有
1.2 ref 传参
概念:std::ref 是 <functional> 里的一个小工具,把“值”变成“引用包装器”,解决 std::thread 默认拷贝参数 带来的“引用的值传不过去”的问题。
std::thread 的默认行为:一律拷贝
void f(int& x) { ++x; }
int a = 0;
std::thread t(f, a); // 编译错误
std::thread构造函数会把所有实参 按值拷贝 到线程内部存储,然后在线程上下文里 以副本为参数 去调用f。- 因此形参
int&绑不到副本,类型不匹配,编译器直接报错。
std::ref 干的事:显式生成“引用包装”
std::thread t(f, std::ref(a)); // OK
std::ref(a)返回一个std::reference_wrapper<int>,它行为像指针,但使用像引用,可以拷贝,内部只存一个int*。- 线程内部存储的是这个轻量级包装器,真正调用
f时,reference_wrapper会隐式转换成int&,于是f拿到的就是主线程里的原始变量a,实现跨线程共享同一块数据。
示例
void Print(int n, int <x, mutex& rmtx) {
rmtx.lock();
for (int i = 0; i < n; ++i) {
++ltx;
}
rmtx.unlock();
}
int main() {
int ltx = 0;
mutex rmtx;
thread t1(Print, 100000, ref(ltx), ref(rmtx));
thread t2(Print, 200000, ref(ltx), ref(rmtx));
t1.join();
t2.join();
return 0;
}
- 如果不加
ref,ltx和rmtx会被拷贝进线程对象;两个线程各自改自己的副本,主线程最后看到的ltx还是 0,而且互斥量也不再是同一把,锁失去意义。 - 加上
ref后,两线程共享同一份ltx和 同一把mutex,才能正确累加并保证线程安全。
或者lambda 表达式捕捉外层对象,也可以不用传递参数,间接解决上面问题
int main() {
int x = 0;
mutex mtx;
auto Print = [&x, &mtx](size_t n) {
mtx.lock();
for (size_t i = 0; i < n; i++) {
++x;
}
mtx.unlock();
};
thread t1(Print, 100000);
thread t2(Print, 200000);
t1.join();
t2.join();
cout << x << endl;
}
🚨 为什么不推荐直接使用 lock() 和 unlock()?
虽然可以手动使用 lock() 和 unlock(),但在实际编程中,强烈不建议这样做。
主要原因在于异常安全和代码维护:
- 忘记解锁: 如果在
lock()和unlock()之间的临界区代码中,你忘记写unlock(),或者程序抛出了异常,那么unlock()将不会被执行,导致mutex永远处于锁定状态(死锁),其他等待线程永远无法运行。 - RAII 缺失: 手动管理资源(锁)容易出错。
为了解决这个问题,C++11 引入了基于 RAII (Resource Acquisition Is Initialization) 机制的锁封装。
2. lock_guard
1. 核心作用
std::lock_guard 是一个轻量级的类模板,它的唯一目的就是实现互斥量 (Mutex) 的自动锁定和解锁。
- 头文件:
<mutex> - 功能: 确保在代码的特定作用域内,互斥量始终处于锁定状态,一旦离开该作用域,互斥量就会被安全释放。
2. RAII 原理
lock_guard 遵循 RAII 原则,它在内部通过以下两个机制实现自动管理:
| 机制 | 描述 |
|---|---|
| 构造函数 | 当 std::lock_guard 对象被创建时,它的构造函数会接收一个 std::mutex 对象的引用,并立即调用该 mutex 的 lock() 成员函数进行加锁。 |
| 析构函数 | 当 std::lock_guard 对象 生命周期 结束(即程序流离开了它所在的代码块/作用域)时,它的析构函数会被自动调用,从而调用关联 mutex 的 unlock() 成员函数进行解锁。 |
3. 最大的优势:异常安全
这是使用 std::lock_guard 相比手动调用 lock() 和 unlock() 的最大优势。
假设临界区代码中抛出了一个异常:
- 手动管理 (
mtx.lock()/mtx.unlock()): 如果在lock()和unlock()之间发生异常,unlock()将永远不会被执行,导致锁永远得不到释放,所有等待该锁的线程将永久阻塞(死锁)。 - 使用
std::lock_guard: 无论代码如何退出作用域(正常执行完毕,或通过异常展开),lock_guard对象的析构函数都会被调用,从而安全地释放互斥量,避免了死锁。
使用 std::lock_guard 非常简单,只需两步:
步骤一:定义互斥量
首先,定义一个 std::mutex 对象。这个对象应该能够被所有需要保护共享数据的线程访问到(通常是全局变量、静态成员或类成员)。
std::mutex mtx; // 定义互斥量
int shared_resource = 0;
步骤二:创建 lock_guard 对象
在需要进入临界区的地方,创建 std::lock_guard 对象,并将互斥量作为构造函数参数传入。
void update_resource() {
// 创建 lock_guard 对象。
// 在这里 mtx.lock() 被调用,线程获得锁。
std::lock_guard<std::mutex> lock(mtx);
// ================== 临界区开始 ==================
shared_resource++;
// 如果这里抛出异常,或者函数正常返回,都不会影响安全解锁。
// ================== 临界区结束 ==================
} // lock_guard 对象在这里被销毁,mtx.unlock() 自动被调用,释放锁。
示例代码
int main() {
int x = 0;
mutex mtx;
auto Print = [&x, &mtx](size_t n) {
// 定义一个局部域
{
lock_guard<mutex> lock(mtx);
for (size_t i = 0; i < n; i++) {
++x;
}
}
for (size_t i = 0; i < 2; i++) {
cout << x << endl;
}
};
thread t1(Print, 100000);
thread t2(Print, 200000);
t1.join();
t2.join();
return 0;
}
3. unique_lock
3.1 基本概述
std::unique_lock 核心特性:在于它对锁的所有权提供了精细的控制
- 延迟锁定 (Deferred Locking): 可以在构造时选择不立即加锁。
- 手动控制: 支持在对象的生命周期内,手动调用
lock()和unlock()。 - 所有权移动: 它是可移动的 (Moveable),可以转移锁的所有权。
- 与条件变量的配合: 它是与
std::condition_variable配合使用的必需品。
构造函数与锁定模式
std::unique_lock 提供了多种构造函数来控制何时加锁:
| 模式 | 描述 | 用途 |
|---|---|---|
| 默认(即时锁定) | std::unique_lock<std::mutex> lock(mtx); |
构造时立即调用 mtx.lock()。与 lock_guard 行为类似。 |
| 延迟锁定 | std::unique_lock<std::mutex> lock(mtx, std::defer_lock); |
只管理 mtx,但不立即加锁。需要稍后手动调用 lock.lock()。 |
| 尝试锁定 | std::unique_lock<std::mutex> lock(mtx, std::try_to_lock); |
尝试调用 mtx.try_lock()。如果成功,则拥有锁;否则不拥有。 |
| 采用锁 | std::unique_lock<std::mutex> lock(mtx, std::adopt_lock); |
假定 mtx 已经被当前线程锁定,unique_lock 接管所有权,并在析构时释放。 |
3.2 用法
A. 延迟锁定和手动控制
使用延迟锁定,你可以将锁定的时间窗口精确到最小范围,从而提高并发性能。
std::mutex mtx;
void do_work() {
// 1. 延迟锁定:只关联 mutex,不立即加锁
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// ----------------------------------------------------
// 在这里执行一些不需要锁的预处理工作 (提高并发)
// ----------------------------------------------------
// 2. 需要访问共享资源时,手动加锁
lock.lock();
// 访问共享资源 (临界区)
std::cout << "Locked and accessing shared resource.\n";
// 3. 可以在临界区结束前手动解锁 (可选)
// lock.unlock();
// ----------------------------------------------------
// 在这里执行一些不需要锁的后处理工作 (提高并发)
// ----------------------------------------------------
} // 离开作用域时,如果锁当前处于锁定状态,析构函数自动解锁。
B. 所有权移动 (Move Semantics)
std::unique_lock 可以像智能指针一样,将锁的所有权从一个对象转移到另一个对象。这在编写返回锁的函数时非常有用。
// 函数返回一个 unique_lock,但锁的所有权被转移给了调用方
std::unique_lock<std::mutex> get_locked_resource() {
std::unique_lock<std::mutex> lock(mtx);
// ... 在这里对资源进行初始化操作 ...
return lock; // 返回时使用移动语义
}
void use_resource() {
// 调用方接收锁的所有权,并保持锁定状态
std::unique_lock<std::mutex> lock = get_locked_resource();
// 在这里继续访问资源... 锁仍然有效
std::cout << "Continuing to use resource with transferred lock.\n";
} // lock 在这里析构并释放锁
C. 与 std::condition_variable 配合 (最重要应用)
这是 std::unique_lock 最关键的应用场景。
条件变量 (std::condition_variable) 用于线程间的等待和通知。当一个线程调用条件变量的 wait() 函数时,它需要原子性地完成两个动作:
- 释放持有的互斥量。
- 阻塞自身并进入等待状态。
std::unique_lock 是唯一能够满足这一需求的锁封装。它提供了必要的灵活性(比如获取内部的 mutex 引用、手动释放锁),以供 condition_variable 内部操作。
简单示例如下:
std::mutex mtx;
std::condition_variable cv;
bool data_ready = false;
// 等待线程
void consumer() {
std::unique_lock<std::mutex> lock(mtx); // 1. 锁定
// 2. 释放锁并等待条件成立。
// cv.wait() 会原子性地 (1) 释放 lock 的 mtx,(2) 阻塞线程。
// 当被唤醒时,它会 (3) 重新锁定 mtx。
cv.wait(lock, []{ return data_ready; });
// ... 处理数据 ...
std::cout << "Consumer: Data processed.\n";
}
3.3 lock_guard vs unique_lock
虽然 std::lock_guard 和 std::unique_lock 都提供 RAII 锁管理,但它们的灵活性不同:
| 特性 | std::lock_guard | std::unique_lock |
|---|---|---|
| 基本目的 | 简单、严格的作用域锁定。 | 灵活、可控的作用域锁定。 |
| 构造时机 | 必须在构造时加锁(除了 std::adopt_lock 模式)。 |
可以延迟锁定 (std::defer_lock)。 |
| 手动控制 | 不支持手动调用 lock() / unlock()。 |
支持手动调用 lock() / unlock()。 |
| 所有权 | 不可移动,不可拷贝。 | 可移动,可将所有权转移给另一个 unique_lock 对象。 |
| 适用场景 | 90% 的情况:只需要简单地在进入作用域时加锁,离开时解锁。 | 需要更复杂的操作,如: 1. 与 std::condition_variable 配合。 2. 延迟锁定或定时锁定。 3. 在函数间传递锁的所有权。 |
| 内存开销 | 更小(通常)。 | 略大(因为它存储了更多状态信息)。 |
4. 函数模板:std::lock 和 std::try_lock
| 特性 | std::lock(mtx1, mtx2, …) | std::try_lock(mtx1, mtx2, …) |
|---|---|---|
| 功能 | 保证无死锁地获得所有锁。 | 尝试原子性地获得所有锁,绝不阻塞。 |
| 行为 | 阻塞(等待直到成功)。 | 非阻塞(立即返回)。 |
| 返回值 | void(成功)。 |
int(-1 成功,非负索引失败)。 |
| 死锁风险 | 无(函数内部实现了死锁避免算法)。 | 无(失败时自动释放已获得锁)。 |
| 后续操作 | 必须配合 std::adopt_lock 来实现 RAII 管理。 |
必须配合 std::adopt_lock 来实现 RAII 管理。 |
注意:这里 lock 和 try_lock 和 mutex 类中的 不一样
函数模板:std::lock
template <class Mutex1, class Mutex2, class... Mutexes>
void lock (Mutex1& a, Mutex2& b, Mutexes&... cde);
std::lock 的核心目标是:原子性地(或称无死锁地)锁定所有传入的互斥量。
工作原理(死锁避免):采用了一种特殊的死锁避免算法(例如,可能使用 “等待-释放” 策略),来确保即使多个线程同时调用 std::lock 锁定同一组互斥量,也不会发生死锁。
- 它会尝试锁定所有的互斥量。
- 如果它在锁定过程中遇到一个已经被占用的互斥量(即阻塞),它会自动释放所有它已经成功获得的锁,然后进入阻塞状态,等待时机重新尝试从头开始锁定所有互斥量。
- 这个过程会重复进行,直到它成功获得所有互斥量。
使用方法:std::lock 函数返回 void,意味着它要么成功锁定所有互斥量,要么永远阻塞直到成功。
示例
std::mutex mtx1, mtx2;
void safe_swap(int& a, int& b) {
// 自动以无死锁的方式同时锁定 mtx1 和 mtx2
std::lock(mtx1, mtx2);
// 注意: 锁已获得,但现在需要使用 RAII 机制来管理解锁!
// 必须使用 std::unique_lock 或 std::lock_guard 配合 std::adopt_lock
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock); // 采用锁:假定 mtx1 已经被锁定
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock); // 采用锁:假定 mtx2 已经被锁定
// 临界区
std::swap(a, b);
// 离开作用域时,lock1 和 lock2 会安全地自动调用 mtx1.unlock() 和 mtx2.unlock()
}
📌 关键点: std::lock 只是帮你获得了锁,它不会自动释放。因此需要一个一个的 unlock,或者配合 std::lock_guard 或 std::unique_lock 和 std::adopt_lock 标签来接管锁的所有权,确保 RAII 机制在退出作用域时安全解锁。
函数模板:std::try_lock
template <class Mutex1, class Mutex2, class... Mutexes>
int try_lock (Mutex1& a, Mutex2& b, Mutexes&... cde);
std::try_lock 的核心目标是:尝试原子性地锁定所有传入的互斥量,且绝不阻塞。
工作原理(非阻塞):它会依次尝试锁定传入的每一个互斥量。
- 成功锁定: 如果所有互斥量都被成功锁定,函数返回
-1。 - 锁定失败: 如果在锁定第 N N N 个互斥量时失败(发现它已经被占用):
- 函数会立即释放所有已经被成功锁定的前 N − 1 N-1 N−1 个互斥量。
- 函数返回第一个未能锁定的互斥量的索引(从 0 开始)。
示例
std::mutex mtxA,mtxB;
void concurrent_task() {
int failed_index = std::try_lock(mtxA, mtxB);
if (failed_index == -1) {
// 成功获得全部锁
std::cout << "Successfully acquired both locks.\n";
// 同样,必须使用 adopt_lock 来接管和管理锁
std::unique_lock<std::mutex> lockA(mtxA, std::adopt_lock);
std::unique_lock<std::mutex> lockB(mtxB, std::adopt_lock);
// ... 执行临界区代码 ...
} else {
// 失败,没有锁被持有 (std::try_lock 已经自动释放了已获得的锁)
std::cout << "Failed to acquire lock at index: " << failed_index << ". Doing other work.\n";
// ... 执行非阻塞的工作 ...
}
}
5. call_once
5.1 基本概述
1. 核心作用:单次执行
std::call_once 的作用非常专一:保证某个可调用对象(函数、Lambda、函数对象)在程序运行期间,无论被多少个线程调用多少次,都只会被执行且只会被成功执行一次。
2. 必备搭档:std::once_flag
std::call_once 总是与 std::once_flag 类型对象一起使用。
std::once_flag: 这是一个同步原语,用于跟踪call_once所关联的函数是否已经被成功调用过。- 机制:
std::call_once函数接收一个std::once_flag引用作为第一个参数,并利用该flag来判断是否需要执行后面的可调用对象。
3. 线程安全与性能
std::call_once 提供了比手动加锁更高的保证和效率:
- 线程安全: 它是完全线程安全的。当多个线程同时竞争调用
call_once时,只有一个线程会成功执行函数,其他线程会被阻塞等待,直到执行线程返回。 - 低开销: 一旦函数被成功执行一次,后续的调用开销非常小,因为它不再需要进行互斥量的加锁/解锁操作。
5.2 使用
std::call_once 的用法非常直观。它需要两个主要部分:一个 std::once_flag 对象 和 要执行的可调用对象。
std::once_flag必须是全局可见的(通常是全局变量或类的静态成员),并且通常使用默认构造函数初始化。
#include <mutex> // std::call_once 和 std::once_flag 都在这里
#include <thread>
#include <iostream>
// 1. 定义 once_flag 对象,作为状态追踪器
std::once_flag initialization_flag;
void Init_system(int value) {
// 假设这是一个耗时且必须只执行一次的初始化函数
std::cout << "System initialized by thread ID: "
<< std::this_thread::get_id()
<< " with value: " << value << "\n";
}
void Worker_task(int id, int init_val) {
std::cout << "Thread " << id << " starts.\n";
// 2. 调用 call_once
// 传入 flag,要执行的函数,以及函数的参数
std::call_once(initialization_flag, Init_system, init_val);
std::cout << "Thread " << id << " continues, initialization guaranteed.\n";
}
int main() {
// 启动多个线程,它们都尝试调用 worker_task,进而调用 call_once
std::thread t1(Worker_task, 1, 100);
std::thread t2(Worker_task, 2, 200);
std::thread t3(Worker_task, 3, 300);
t1.join();
t2.join();
t3.join();
return 0;
}
运行结果(示例):
Thread 1 starts.
Thread 2 starts.
System initialized by thread ID: 1234567890 with value: 100
Thread 3 starts.
Thread 1 continues, initialization guaranteed.
Thread 2 continues, initialization guaranteed.
Thread 3 continues, initialization guaranteed.
观察结果: 尽管所有三个线程都调用了
std::call_once,但initialize_system函数只被线程 1(第一个成功竞争到锁的线程)执行了一次,且参数为 100。
🎯 典型应用:线程安全的单例模式
std::call_once 最经典的用途就是实现线程安全的惰性初始化单例模式 (Lazy Initialization Singleton)。
传统 C++11 之前的单例问题: 传统的单例在多线程下需要手动加锁来保证 new Singleton() 只被调用一次,否则可能产生多个实例。
使用 std::call_once 实现:
class Singleton {
private:
Singleton() = default;
static Singleton* instance_;
static std::once_flag flag_;
public:
static Singleton* getInstance() {
// 使用 call_once 保证初始化函数只被执行一次
std::call_once(flag_, []() {
instance_ = new Singleton();
});
return instance_;
}
// ... 其他单例标准方法 ...
};
// 静态成员初始化
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::flag_;
5.3 补充:C++11 后的更简洁单例
需要注意的是,自 C++11 起,还有一种更简洁、更推荐的线程安全单例实现方法:Magic Statics (神奇的静态变量)。
class Singleton {
private:
Singleton() = default;
public:
static Singleton& getInstance() {
// C++11 标准保证:函数内静态变量的局部初始化是线程安全的
static Singleton instance;
return instance;
}
};
在这种情况下,甚至不需要 std::call_once,因为编译器为你完成了线程安全保证。但 std::call_once 在需要执行一个复杂的、带参数的初始化函数时,仍然是不可替代的。
【★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新相关的内容,请持续关注我 !!

更多推荐



所有评论(0)