在这里插入图片描述

📃个人主页: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_guardstd::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) 的所有权语义。这意味着:
    1. 一旦一个线程锁定了 mutex,其他试图锁定它的线程都会被阻塞(直到锁被释放)。
    2. 锁定它的线程不能在释放之前再次锁定它(非递归),否则会导致 未定义行为死锁

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_fortry_lock_untile 的接口,这两个接口跟try_lock类似,只是他不会马上返回,而是直接进入阻塞,直到时间条件到了或者解锁了就会唤醒试图获取锁资源。
  • recursive_mutex 跟mutex完全类似,recursive_mutex 提供 排他性递归所有权语义:
    1. 调用方线程在从它成功调用 lock 或 trylock开始的时期里占有 recursive_mutex。此时期之内,线程可以进行对 locktry_lock 的附加调用。所有权的时期在线程进行匹配次数的 unlock 调用时结束。
    2. 线程占有 recursive_mutex 时,若其他所有线程试图要求 recursive_mutex 的所有权则它们将阻塞(对于调用 lock)或收到 false 返回值(对于调用 try _lock)
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 &ltx, 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;
}
  • 如果不加 refltxrmtx 会被拷贝进线程对象;两个线程各自改自己的副本,主线程最后看到的 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(),但在实际编程中,强烈不建议这样做。

主要原因在于异常安全代码维护

  1. 忘记解锁: 如果在 lock()unlock() 之间的临界区代码中,你忘记写 unlock(),或者程序抛出了异常,那么 unlock() 将不会被执行,导致 mutex 永远处于锁定状态(死锁),其他等待线程永远无法运行。
  2. 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 对象的引用,并立即调用该 mutexlock() 成员函数进行加锁。
析构函数 std::lock_guard 对象 生命周期 结束(即程序流离开了它所在的代码块/作用域)时,它的析构函数会被自动调用,从而调用关联 mutexunlock() 成员函数进行解锁。

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 核心特性:在于它对锁的所有权提供了精细的控制

  1. 延迟锁定 (Deferred Locking): 可以在构造时选择不立即加锁。
  2. 手动控制: 支持在对象的生命周期内,手动调用 lock()unlock()
  3. 所有权移动: 它是可移动的 (Moveable),可以转移锁的所有权。
  4. 与条件变量的配合: 它是与 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() 函数时,它需要原子性地完成两个动作:

  1. 释放持有的互斥量。
  2. 阻塞自身并进入等待状态。

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_guardstd::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 锁定同一组互斥量,也不会发生死锁。

  1. 它会尝试锁定所有的互斥量。
  2. 如果它在锁定过程中遇到一个已经被占用的互斥量(即阻塞),它会自动释放所有它已经成功获得的锁,然后进入阻塞状态,等待时机重新尝试从头开始锁定所有互斥量。
  3. 这个过程会重复进行,直到它成功获得所有互斥量。

使用方法: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_guardstd::unique_lockstd::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. 成功锁定: 如果所有互斥量都被成功锁定,函数返回 -1
  2. 锁定失败: 如果在锁定第 N N N 个互斥量时失败(发现它已经被占用):
    • 函数会立即释放所有已经被成功锁定的前 N − 1 N-1 N1 个互斥量
    • 函数返回第一个未能锁定的互斥量的索引(从 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 在需要执行一个复杂的、带参数的初始化函数时,仍然是不可替代的。

★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新相关的内容,请持续关注我 !!

在这里插入图片描述

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐