跳到主要内容
版本:1.0

mutex

std::mutex 是 C++11 提供的最基础互斥量,用来保护多个线程之间共享的数据,确保同一时刻只有一个线程能进入临界区。

如果多个线程同时读写同一份共享状态,而中间又没有同步手段,就会产生数据竞争(data race)。mutex 的核心作用,就是把这段必须串行执行的代码保护起来。

头文件与基本特征

使用 mutex 时,通常包含:

#include <mutex>

它的常见特征如下:

特征说明
角色保护共享资源,避免多个线程同时进入临界区
访问方式同一时刻只允许一个线程持有锁
基本接口lock()try_lock()unlock()
所有权加锁线程负责解锁,不能跨线程随意 unlock()
递归加锁std::mutex 不支持同一线程重复加锁
常见搭配std::lock_guardstd::unique_lockstd::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 的过程,方便直观看到:

  1. lock() 在锁被占用时会进入等待状态。
  2. try_lock() 失败时会立即返回。
  3. 只有持有锁的线程才能安全修改共享数据。
  4. 非持有线程解锁属于错误用法。

std::mutex 竞争演示

模拟两个线程竞争同一个互斥量,并观察 lock、try_lock、unlock 的行为。

MDX + React
ownernone
shared counter0
waiting threadsempty
状态说明当前没有线程持有 mutex,任一线程都可以尝试 lock() 或 try_lock()。
推荐写法实际工程中优先用 lock_guard 或 unique_lock 管理锁生命周期。

线程 A

线程 B

控制

最近操作

  • 初始化: 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 与死锁

当多个线程需要同时锁住多个互斥量时,如果加锁顺序不一致,就可能死锁。

例如:

  1. 线程 A 先锁 m1,再锁 m2
  2. 线程 B 先锁 m2,再锁 m1
  3. 两边都拿到一个锁并等待另一个锁时,就会互相卡住。

一种常见做法是统一加锁顺序;另一种是使用 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 如何选择

这两个工具都能处理并发问题,但适用范围不同。

  1. 只保护一个简单标志位或计数器,而且操作本身可以原子完成时,优先考虑 std::atomic
  2. 需要把多个变量作为一个整体维护一致性时,通常更适合 std::mutex
  3. 临界区里不只是读写一个值,而是一整段逻辑时,mutex 更自然。
  4. 需要和等待通知机制配合时,通常是 mutex + condition_variable

可以把它简单理解为:atomic 保护“单个原子动作”,mutex 保护“一整段临界区”。

常见场景

  1. 保护共享计数器、队列、链表、映射表等共享数据结构。
  2. 保证日志输出、文件写入等操作不会被多个线程打断交错。
  3. 把多个变量视为一个整体状态进行同步更新。
  4. condition_variable 搭配实现生产者消费者模型。

使用注意

  1. 优先使用 lock_guardunique_lock,不要轻易手写成对的 lock() / unlock()
  2. 同一线程重复 lock() 同一个 std::mutex 属于未定义行为,实践中常表现为死锁。
  3. 不是持有者的线程去 unlock(),或者在锁还持有时销毁 mutex,都属于未定义行为。
  4. 临界区要尽量短,不要把耗时计算、I/O、复杂回调长期放在锁内。
  5. 持锁期间如果还要调用外部代码,要特别小心,因为这类代码可能再次加锁或阻塞,引出更隐蔽的问题。
  6. 如果只是对一个简单整数做计数,未必一定要用 mutex,有时 atomic 更直接。

小结

std::mutex 是 C++11 并发编程里最基础也最重要的同步工具之一。它的核心任务不是“让代码变慢一点”,而是明确共享资源的访问边界,避免数据竞争。

掌握 lock()try_lock()unlock() 这些基本接口之后,更重要的是形成一个习惯:在工程里优先通过 lock_guardunique_lock 这类 RAII 方式管理锁。这样代码不仅更安全,也更容易维护。