【C++11并发编程】mutex 锁
为什么需要 “锁” ?
- 在多线程编程中,多个线程可能同时访问同一块内存(比如一个全局变量),这会导致 数据竞争(Data Race) ,从而产生不可预测的结果。举个例子:
如下代码中,两个线程同时运行 increment(),最终 counter 的值很可能 不是 200000,因为 counter++ 实际上包含三步:
1、读取 counter 的值;
2、加 1;
3、写回新值。
如果两个线程交错执行这三步,就会互相覆盖结果,如下所示两次执行的结果不一样,结果也不是200000。
PS:这里有个小疑问,如果 将 100000 换成一个比较小的数,例如 100,那么它的结果是确定的 200 ,这个疑问先暂存。
#include <iostream>
#include <thread>
using namespace std;
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // 这行代码不是原子操作!
}
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
cout << counter << endl;
}

-
C++11 标准(2011年发布)之前,C++ 没有标准的线程或同步机制,开发者只能依赖平台特定的 API(如 POSIX pthread、Windows API),导致代码不可移植。
C++11 首次引入了标准的多线程支持,包括<thread>、<mutex>等。 -
✅ 锁(Lock)的作用:确保同一时间只有一个线程能执行某段关键代码(称为“临界区”),从而避免数据竞争 。
-
同一时间只能有一个线程持有锁!!
一、锁
头文件:<mutex>
1.1 mutex 互斥锁
基本介绍
- 作用
最基本的锁,用于保护临界区,确保同一时间只有一个线程能进入。 - 特性
- 不可递归:⚠️ 同一线程重复调用 lock() 会导致未定义行为(通常死锁)。
- ⚠️ mutex 对象不可拷贝、赋值、移动。
- 非公平锁 :⚠️ 当多个线程在等待同一个 mutex 时,先等的线程不一定先拿到锁。
这是因为:当一个线程释放 mutex 时,操作系统或底层实现可能直接把锁交给刚刚被唤醒的线程,或者正在运行的线程,而不是从等待队列头部取。
这样做的好处是:减少上下文切换、提高吞吐量(性能更好)。坏处是:某些线程可能“饿死”(长时间等不到锁),虽然实际中很少见
- 使用建议
⚠️ 永远不要直接使用 mutex::lock()/unlock() !应配合 RAII 封装类(如 lock_guard)使用,避免异常导致死锁。 - 原型
class mutex {
public:
constexpr mutex() noexcept; // 默认构造
~mutex(); // 析构(必须处于 unlocked 状态)
mutex(const mutex&) = delete; // 禁止拷贝
mutex& operator=(const mutex&) = delete; // 禁止赋值
void lock(); // 阻塞直到获得锁
bool try_lock() noexcept; // 尝试加锁,成功返回 true,否则返回 false
void unlock(); // 解锁(必须由持有锁的线程调用)
};
▶ mutex 创建锁
- 样例
#include <mutex>
using namespace std;
mutex Mymutex1;
mutex Mymutex2 = Mymutex1; // 报错!⚠️ 禁止赋值
mutex Mymutex3(Mymutex1); // 报错!⚠️ 禁止拷贝构造
mutex Mymutex4 = move(Mymutex1); // 报错!⚠️ 禁止移动
▶ lock() 加锁(阻塞)
void lock();
- 样例
Mymutex1.lock();
- 行为:若当前未被锁定,则加锁并返回;否则 阻塞 当前线程,直到其他线程调用 unlock()。
- 异常:若发生错误(如资源不足),可能抛出
std::system_error。 - 注意:⚠️ 不要在已经成功对某个 mutex 加锁的线程中,再次对该同一个 mutex 调用 lock(),会导致 未定义行为!!!。
未定义行为 :程序 可能直接崩溃(比如死锁、段错误),可能陷入无限等待(因为线程在等自己释放锁),也可能看似“正常”运行,但这是偶然的,不可靠的,意味着 C++ 标准 不保证任何结果——编译器、操作系统、运行时环境可以做任何事!
▶ try_lock() 尝试加锁(非阻塞)
bool try_lock() noexcept;
- 样例
Mymutex1.try_lock();
- 行为:尝试立即加锁。若成功(即当前无人持有锁),返回 true 并加锁;否则立即返回 false,不阻塞。
- 异常:声明为
noexcept,不会抛异常。 - 用途:用于避免死锁、实现超时逻辑或非阻塞算法。
▶ unlock() 解锁
void unlock();
- 样例
Mymutex1.unlock();
- ⚠️ 使用前提是:当前线程必须是锁的持有者,且锁处于 locked 锁定 状态;否则会导致 未定义行为!!!
- 行为:释放锁,唤醒一个(或多个)等待线程。
实例
#include <mutex>
#include <iostream>
using namespace std;
mutex Mymutex;
int counter = 0;
void increment() {
Mymutex.lock(); // 结果输出固定为 200000
// Mymutex.try_lock(); // 结果输出 不定
for (int i = 0; i < 100000; ++i) {
counter++;
}
Mymutex.unlock();
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
cout << counter << endl;
}
1.2 recursive_mutex 递归锁
基本介绍
-
背景
为了 解决 同一线程在未释放锁的情况下再次尝试加锁的问题 ,比如上面 对同一线程内 已加锁 的 mutex 再次加锁会产生未定义的行为。常见于函数嵌套调用、公共接口调用内部工具函数等场景。 -
作用
允许同一线程多次成功加锁(lock)同一个互斥量,而不会导致死锁 。 内部通过引用计数实现,每次 lock() 计数 +1,每次 unlock() 计数 -1;仅当计数归零时,锁才真正释放。 每次 lock() 必须对应一次 unlock(),只有当所有锁都被释放后,其他线程才能获取该锁。 -
特性
- ⚠️ recursive_mutex 对象禁止拷贝构造、移动、赋值操作。
- ⚠️ 非公平锁。
- 性能略低于 mutex: 因为 需维护内部锁计数器和线程 ID,有轻微额外开销。
-
使用建议
和 mutex 一样,永远 不要直接调用 lock()/unlock()!应使用 lock_guard、unique_lock 等。 -
原型
class recursive_mutex {
public:
recursive_mutex(); // 默认构造
~recursive_mutex(); // 析构(必须处于 unlocked 状态)
recursive_mutex(const recursive_mutex&) = delete; // 禁止拷贝
recursive_mutex& operator=(const recursive_mutex&) = delete; // 禁止赋值
void lock(); // 阻塞加锁(可重入)
bool try_lock() noexcept; // 尝试加锁(可重入)
void unlock(); // 解锁(必须由持有者调用)
};
▶ recursive_mutex 创建递归锁
- 样例
#include <mutex>
using namespace std;
recursive_mutex rmtx1;
recursive_mutex rmtx2 = rmtx1; // ❌ 报错!禁止拷贝
recursive_mutex rmtx3(rmtx1); // ❌ 报错!禁止拷贝构造
recursive_mutex rmtx4 = move(rmtx1); // ❌ 报错!禁止移动
▶ lock() 加锁(阻塞,可重入)
void lock();
- 样例
rmtx1.lock(); // 第一次:成功
rmtx1.lock(); // 同一线程第二次:成功(计数=2)
- 行为:若当前线程已持有锁,则内部计数 +1 并立即返回;否则阻塞直到获得锁。
- 异常:可能抛出
std::system_error。 - 注意:⚠️ 即使可重入,也必须 确保 unlock() 次数等于 lock() 次数 ,否则锁永不释放!
▶ try_lock() 尝试加锁(非阻塞,可重入)
bool try_lock() noexcept;
- 样例
if (rmtx1.try_lock()) {
// 成功加锁(可能是首次,也可能是重入)
// ...
rmtx1.unlock();
}
- 行为:若当前线程已持有锁,则计数 +1 并返回 true;否则尝试立即加锁,成功返回 true,失败返回 false。
- 异常:声明为
noexcept, 不抛异常。
▶ unlock() 解锁
void unlock();
- 样例
rmtx1.unlock(); // 计数 -1
rmtx1.unlock(); // 再次 -1(若计数归零,则真正释放)
- 前提: ⚠️unlock() 前,当前线程必须是锁的持有者,且锁处于 locked 状态(计数 > 0)。否则会产生未定义行为!!
- 行为:将内部锁计数减 1;若计数变为 0,则释放锁并唤醒等待线程。
实例
#include <iostream>
#include <mutex>
using namespace std;
int main() {
recursive_mutex m;
//mutex m; // 若换成 mutex,程序将死锁!
cout << "Locking first time...\n";
m.lock();
cout << "Locked! Now trying to lock again...\n";
m.lock(); // ← 同一线程再次加锁 → 死锁(在大多数 POSIX 系统上)
cout << "This will NEVER print.\n";
m.unlock();
m.unlock();
}

1.3 自旋锁
- 自旋锁(Spinlock) 是一种 忙等待 的同步机制:当线程尝试获取锁失败时,它不会阻塞或睡眠,而是线程保持运行态, 原地循环检查 即"自旋",直到锁被释放。
- 适用场景
自旋锁是 用 CPU 时间换上下文切换开销 的极端优化,仅在"等待时间远小于切换开销"时有效。绝大多数用户态代码应使用互斥锁,自旋锁主要存在于操作系统内核和极低延迟场景。- 锁持有时间极短(如几条指令)
上下文切换开销可能远大于临界区执行时间 ,自旋反而更快。 - 中断上下文 / 硬中断处理
中断处理程序不能睡眠(阻塞),只能使用自旋锁。 - 多核实时系统
避免调度延迟,保证确定性。 - 内核底层实现
Linux 内核中大量使用自旋锁保护短临界区。
- 锁持有时间极短(如几条指令)
- 致命缺点
- CPU 空转浪费:自旋期间占用核心 100% 利用率。
- 优先级反转:低优先级线程持有锁时,高优先级线程自旋可能饿死低优先级线程(无法释放锁)。
- 单核噩梦:在单核 CPU 上,自旋锁持有者若被抢占,其他线程自旋将 永远等不到(需禁止抢占或中断)。
自旋锁 与 互斥锁 的本质区别
| 特性 | 自旋锁 | 互斥锁 |
|---|---|---|
| 等待机制 | 忙等待(循环检测) | 阻塞等待(让出 CPU) |
| 上下文切换 | 无 | 两次(阻塞+唤醒) |
| CPU 占用 | 高(持续自旋) | 低(线程休眠) |
| 适用场景 | 极短临界区、中断上下文 | 较长临界区、用户空间 |
二、锁管理器
2.1 lock_guard 自动 锁管理器
基本介绍
-
作用
RAII 风格的 互斥锁管理器 。💡 RAII :Resource Acquisition Is Initialization(资源获取即初始化),利用对象生命周期自动管理资源(如锁的释放)。
-
局限性
- ⚠️ 构造即加锁,不能延迟加锁 。
- ⚠️不能手动解锁 :没有 unlock() 方法。
- 若需更灵活控制,请用 unique_lock。
-
lock_guard 本身只是一个 锁管理器,多个锁管理器能否能对同一个 锁 再次加锁,完全取决于这个 锁类型是 否支持再次加锁。 例如:lock_guard<mutex> 就不支持对一个已加锁的 mutex 对象再次加锁。 lock_guard<recursive_mutex> 则支持再次加锁。
mutex m;
lock_guard<mutex> g1(m); // 第一次加锁 → OK
lock_guard<mutex> g2(m); // 同一线程再次加锁同一个 mutex → UB!
- 原型
template <class Mutex>
class lock_guard {
public:
using mutex_type = Mutex;
explicit lock_guard(Mutex& m); // 加锁
lock_guard(Mutex& m, std::adopt_lock_t); // 假设 m 已被当前线程锁定(不加锁)
~lock_guard(); // 自动解锁
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
};
▶ 自动 加/解锁
-
作用:构造时自动加锁,析构时自动解锁。
-
样例
#include <mutex>
using namespace std;
mutex mtx;
lock_guard<mutex> Mylockguard1(mtx); // 自动加锁
lock_guard<mutex> Mylockguard2 = Mylockguard1; // 报错!⚠️ 禁止赋值
mutex Mylockguard3(Mylockguard1); // 报错!⚠️ 禁止拷贝构造
mutex Mylockguard4 = move(Mylockguard1); // 报错!⚠️ 禁止移动
▶ 自动 解锁
-
前提:⚠️所管理的 锁 已提前被加锁。否则会引发未定义行为!
-
作用:构造时 不 自动加锁,仅管理已加锁的锁,析构时自动解锁。
-
样例:
mutex mtx;
lock_guard<mutex> Myadoptlock0(mtx,adopt_lock); // 错误!⚠️ 所管理的锁必须得提前加锁!!!
mtx.lock();
lock_guard<mutex> Myadoptlock(mtx,adopt_lock); // 自动解锁
实例
实例1:自动加锁/解锁
#include <mutex>
#include <iostream>
using namespace std;
mutex mtx;
int counter = 0;
void increment() {
lock_guard<mutex> Mylockguard(mtx);
for (int i = 0; i < 100000; ++i) {
counter++;
}
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
cout << counter << endl; // 输出 200000
}
实例2:自动解锁
- 死锁问题:
#include <mutex>
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
mutex mtxA, mtxB;
void lock_A_then_B() {
cout << "Thread 1: trying to lock A...\n";
lock_guard<mutex> g1(mtxA);
cout << "Thread 1: locked A, now trying to lock B...\n";
// 模拟一些处理时间,增加与另一线程交叉的机会
this_thread::sleep_for(chrono::milliseconds(100));
lock_guard<mutex> g2(mtxB); // 此处可能永远阻塞!
cout << "Thread 1: locked B, doing work...\n";
}
void lock_B_then_A() {
cout << "Thread 2: trying to lock B...\n";
lock_guard<mutex> g1(mtxB);
cout << "Thread 2: locked B, now trying to lock A...\n";
// 模拟处理时间
this_thread::sleep_for(chrono::milliseconds(100));
lock_guard<mutex> g2(mtxA); // 此处可能永远阻塞!
cout << "Thread 2: locked A, doing work...\n";
}
int main() {
thread t1(lock_A_then_B);
thread t2(lock_B_then_A);
t1.join(); // ← 程序会卡在这里(死锁)
cout << "here" << endl; // 这行不会执行
t2.join();
cout << "Program finished.\n"; // 这行也不会执行!
return 0;
}

- 解决方法:
std::lock(mtxA, mtxB) 的作用
这是一个原子操作:它会以某种内部策略(如尝试-回退)确保两个锁被同时获取。
无论线程以什么顺序调用,都不会出现“各持一个、互相等待”的情况。
它保证:要么两个都锁成功,要么不断重试直到成功(不会部分持有)。
#include <mutex>
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
mutex mtxA, mtxB;
// 安全版本:避免死锁
void safe_operation(const string& name) {
cout << name << ": preparing to lock both mutexes...\n";
// Step 1: 使用 lock 同时锁定两个 mutex(无死锁)
lock(mtxA, mtxB);
// Step 2: 用 adopt_lock 告诉 lock_guard:“锁已经加好了,请只负责析构时 unlock”
lock_guard<mutex> g1(mtxA, adopt_lock);
lock_guard<mutex> g2(mtxB, adopt_lock);
cout << name << ": locked both A and B, doing work...\n";
// 模拟工作(现在可以安全休眠,不会导致死锁)
this_thread::sleep_for(chrono::milliseconds(100));
cout << name << ": finished work.\n";
// g1 和 g2 析构时自动 unlock(顺序无关)
}
int main() {
thread t1([]() { safe_operation("Thread 1"); });
thread t2([]() { safe_operation("Thread 2"); });
t1.join();
t2.join();
cout << "Program finished successfully! No deadlock.\n";
return 0;
}

2.2 unique_lock 灵活锁 管理器
基本介绍
-
作用
比 lock_guard 更灵活的锁管理器,支持延迟加锁、手动解锁、转移所有权等 。 -
原型
template <class Mutex>
class unique_lock {
public:
using mutex_type = Mutex;
// (1) ✅️默认构造(无关联 mutex)
unique_lock() noexcept;
// (2) ✅️关联 mutex,但不加锁
explicit unique_lock(Mutex& m, std::defer_lock_t) noexcept;
// (3) ✅️关联并加锁
explicit unique_lock(Mutex& m);
// (4) 关联并尝试加锁(非阻塞)
unique_lock(Mutex& m, std::try_to_lock_t);
// (5) 接管已加锁的 mutex
unique_lock(Mutex& m, std::adopt_lock_t);
// (6) 支持超时(配合 timed_mutex)
unique_lock(Mutex& m, const std::chrono::duration<Rep, Period>& timeout_duration);
unique_lock(Mutex& m, const std::chrono::time_point<Clock, Duration>& timeout_time);
~unique_lock(); // ✅️若仍持有锁,则 unlock()
// 核心操作
void lock();
bool try_lock();
template<class Rep, class Period>
bool try_lock_for(const std::chrono::duration<Rep, Period>& rel_time);
template<class Clock, class Duration>
bool try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time);
void unlock();
// 状态查询
bool owns_lock() const noexcept;
explicit operator bool() const noexcept { return owns_lock(); }
Mutex* mutex() const noexcept;
// 所有权转移(move-only)
unique_lock(unique_lock&& other) noexcept;
unique_lock& operator=(unique_lock&& other) noexcept;
};
▶ unique_lock 的创建
- ⚠️ 支持移动语义,不支持拷贝、赋值!
- 样例
mutex mtx;
unique_lock<mutex> lock1(mtx);
unique_lock<mutex> lock2 = move(lock1); // 移动赋值,现在 lock2 拥有锁,lock1 不再持有
unique_lock<mutex> a(mtx);
unique_lock<mutex> b(lock1); // ❌ 错误!禁止拷贝
unique_lock<mutex> c = lock1; // ❌ 错误!禁止赋值
▶ defer_lock 延迟加锁
unique_lock<mutex> ulock(Mymutelock, defer_lock);
// ... 做些准备 ...
ulock.lock(); // 手动加锁
▶ 配合条件变量(必须用 unique_lock)
condition_variable cv;
mutex mtx;
bool ready = false;
// 等待线程
unique_lock<std::mutex> ulock(mtx);
cv.wait(lock, []{ return ready; }); // wait 会临时 unlock,唤醒后 relock
实例
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <chrono>
std::mutex log_mutex;
std::vector<std::string> shared_log;
void log_message(const std::string& msg) {
// Step 1: 加锁,安全写入内存队列
std::unique_lock<std::mutex> lock(log_mutex);
shared_log.push_back(msg);
std::cout << "Logged: " << msg << " (size=" << shared_log.size() << ")\n";
// Step 2: 暂时释放锁!因为接下来要模拟“写磁盘”(耗时 I/O)
lock.unlock(); // ✅ 关键:释放锁,让其他线程可以继续写日志
// 模拟耗时 I/O 操作(比如写文件、发网络请求)
std::this_thread::sleep_for(std::chrono::milliseconds(500));
// Step 3: 如果需要再次访问 shared_log,可以重新加锁
lock.lock(); // ✅ 重新加锁
if (!shared_log.empty()) {
// 假装做点检查
std::cout << "Still " << shared_log.size() << " messages in buffer.\n";
}
// 析构时自动 unlock(如果还持有)
}
int main() {
std::thread t1([] { log_message("Error: Disk full"); });
std::thread t2([] { log_message("Warning: High CPU"); });
std::thread t3([] { log_message("Info: User logged in"); });
t1.join();
t2.join();
t3.join();
std::cout << "All logs processed. Final buffer size: " << shared_log.size() << "\n";
return 0;
}

三、辅助函数:std::lock()
基本介绍
- 作用
同时锁定多个 mutex,避免死锁 (使用某种死锁避免算法,如“先尝试全部 lock,失败则回退重试”)。 - 原型
template <class Lockable1, class Lockable2, class... LockableN>
void lock(Lockable1& m1, Lockable2& m2, LockableN&... mN);
- 该函数本身不提供 RAII,通常配合
adopt_lock+lock_guard/unique_lock使用。 - C++17 后推荐直接用
scoped_lock。
实例
详见上面 lock_guard 的 实例2。
四、小结
- 会引发未定义行为的动作:
- 当前线程的 std::mutex 已 locked 加锁, 却又再次 lock() 加锁。
- unlock() 前,当前线程必须是锁的持有者,且锁处于 locked 状态,否则会产生未定义行为。
- 使用 lock_guard Myadoptlock(mtx,adopt_lock); 构造自动解锁管理器时,所管理的锁未提前加锁,
更多推荐

所有评论(0)