mutex
std::mutex 是 C++11 提供的最基础互斥量,用来保护多个线程之间共享的数据,确保同一时刻只有一个线程能进入临界区。
如果多个线程同时读写同一份共享状态,而中间又没有同步手段,就会产生数据竞争(data race)。mutex 的核心作用,就是把这段必须串行执行的代码保护起来。
头文件与基本特征
使用 mutex 时,通常包含:
#include <mutex>
它的常见特征如下:
| 特征 | 说明 |
|---|---|
| 角色 | 保护共享资源,避免多个线程同时进入临界区 |
| 访问方式 | 同一时刻只允许一个线程持有锁 |
| 基本接口 | lock()、try_lock()、unlock() |
| 所有权 | 加锁线程负责解锁,不能跨线程随意 unlock() |
| 递归加锁 | std::mutex 不支持同一线程重复加锁 |
| 常见搭配 | std::lock_guard、std::unique_lock、std::condition_variable |
为什么需要 mutex
看一个最常见的共享计数器场景:
#include <thread>
int counter = 0;
void work() {
for (int i = 0; i < 100000; ++i) {
++counter;
}
}
如果两个线程同时执行 work(),++counter 并不是原子操作,中间可能发生读写交错,最终结果就可能小于预期值。
这时就需要用互斥量把更新过程保护起来。
基本用法
#include <iostream>
#include <mutex>
#include <thread>
std::mutex mtx;
int counter = 0;
void work() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
}
int main() {
std::thread t1(work);
std::thread t2(work);
t1.join();
t2.join();
std::cout << counter << '\n';
return 0;
}
这里最关键的一行是:
std::lock_guard<std::mutex> lock(mtx);
它会在当前作用域开始时自动加锁,在离开作用域时自动解锁,比手动 lock() / unlock() 更安全。
交互演示(MDX + React)
下面这个面板会模拟两个线程竞争同一个 std::mutex 的过程,方便直观看到:
lock()在锁被占用时会进入等待状态。try_lock()失败时会立即返回。- 只有持有锁的线程才能安全修改共享数据。
- 非持有线程解锁属于错误用法。
std::mutex 竞争演示
模拟两个线程竞争同一个互斥量,并观察 lock、try_lock、unlock 的行为。
最近操作
- 初始化: mutex 为空闲状态,A 和 B 都可以尝试进入临界区
说明:这个交互示例是教学用的状态模拟,不是执行真实 C++ 线程代码;它的目的是帮助理解互斥量的所有权和临界区概念。
常用成员函数
| 函数 | 作用 |
|---|---|
lock() | 获取互斥量,若已被占用则阻塞等待 |
try_lock() | 尝试获取互斥量,失败时立即返回 false |
unlock() | 释放互斥量 |
native_handle() | 获取底层实现句柄,通常只在特定平台场景使用 |
lock 和 unlock
最直接的写法是手动加锁和解锁:
#include <mutex>
std::mutex mtx;
int value = 0;
void update() {
mtx.lock();
++value;
mtx.unlock();
}
这种写法虽然直观,但有一个明显问题:一旦中间出现异常、提前 return,或者后续逻辑变复杂,就很容易忘记解锁。
所以在实际代码里,通常更推荐 RAII 风格。
try_lock
try_lock() 适合“拿不到锁就先做别的事”的场景:
#include <mutex>
std::mutex mtx;
void tryWork() {
if (mtx.try_lock()) {
// 成功拿到锁,安全访问共享资源
mtx.unlock();
} else {
// 没拿到锁,立即返回或改做其它工作
}
}
和 lock() 不同,try_lock() 不会等待,它更适合低延迟或轮询式控制逻辑。
lock_guard 与 unique_lock
在现代 C++ 里,互斥量通常不会裸着使用,而是搭配 RAII 包装器。
lock_guard
std::lock_guard 是最轻量、最直接的选择:
#include <mutex>
std::mutex mtx;
void safeWork() {
std::lock_guard<std::mutex> lock(mtx);
// 临界区
}
它的特点是简单、稳定、几乎没有额外控制成本,适合绝大多数“进入作用域就加锁,离开作用域就解锁”的场景。
unique_lock
std::unique_lock 更灵活,可以手动 unlock() / lock(),也可以和条件变量配合:
#include <condition_variable>
#include <mutex>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void waitForReady() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return ready; });
}
如果你只是做简单互斥,优先 lock_guard;如果需要延迟加锁、临时释放锁,或者要和 condition_variable 搭配,就使用 unique_lock。
多个 mutex 与死锁
当多个线程需要同时锁住多个互斥量时,如果加锁顺序不一致,就可能死锁。
例如:
- 线程 A 先锁
m1,再锁m2。 - 线程 B 先锁
m2,再锁m1。 - 两边都拿到一个锁并等待另一个锁时,就会互相卡住。
一种常见做法是统一加锁顺序;另一种是使用 std::lock 一次性锁住多个互斥量:
#include <mutex>
std::mutex m1;
std::mutex m2;
void safeTask() {
std::lock(m1, m2);
std::lock_guard<std::mutex> lock1(m1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);
// 同时安全持有 m1 和 m2
}
std::adopt_lock 的含义是:“这个锁已经拿到了,你现在接管它的解锁责任。”
mutex 与 atomic 如何选择
这两个工具都能处理并发问题,但适用范围不同。
- 只保护一个简单标志位或计数器,而且操作本身可以原子完成时,优先考虑
std::atomic。 - 需要把多个变量作为一个整体维护一致性时,通常更适合
std::mutex。 - 临界区里不只是读写一个值,而是一整段逻辑时,
mutex更自然。 - 需要和等待通知机制配合时,通常是
mutex + condition_variable。
可以把它简单理解为:atomic 保护“单个原子动作”,mutex 保护“一整段临界区”。
常见场景
- 保护共享计数器、队列、链表、映射表等共享数据结构。
- 保证日志输出、文件写入等操作不会被多个线程打断交错。
- 把多个变量视为一个整体状态进行同步更新。
- 和
condition_variable搭配实现生产者消费者模型。
使用注意
- 优先使用
lock_guard或unique_lock,不要轻易手写成对的lock()/unlock()。 - 同一线程重复
lock()同一个std::mutex属于未定义行为,实践中常表现为死锁。 - 不是持有者的线程去
unlock(),或者在锁还持有时销毁mutex,都属于未定义行为。 - 临界区要尽量短,不要把耗时计算、I/O、复杂回调长期放在锁内。
- 持锁期间如果还要调用外部代码,要特别小心,因为这类代码可能再次加锁或阻塞,引出更隐蔽的问题。
- 如果只是对一个简单整数做计数,未必一定要用
mutex,有时atomic更直接。
小结
std::mutex 是 C++11 并发编程里最基础也最重要的同步工具之一。它的核心任务不是“让代码变慢一点”,而是明确共享资源的访问边界,避免数据竞争。
掌握 lock()、try_lock()、unlock() 这些基本接口之后,更重要的是形成一个习惯:在工程里优先通过 lock_guard、unique_lock 这类 RAII 方式管理锁。这样代码不仅更安全,也更容易维护。