std::mutex
cpp
#include <mutex>
using std::mutex;
互斥量 ,用于在多线程环境下保护共享资源,确保同一时刻只有一个线程 进入临界区,从而避免数据竞争和部分并发导致的未定义行为。
典型场景:多个线程同时读写同一个 std::vector、计数器、对象状态等。
std::mutex 本身:
- 表达共享资源的 "被锁的状态 + 所有权"
- 同一时刻最多只能被 一个线程持有
基本用法
cpp
std::mutex m;
m.lock(); // m上锁
m.unlock(); // m 解锁
m.try_lock(); // 尝试获取锁,不阻塞;成功返回 `true`,失败返回 `false`。
mutex理解
问题代码示例
cpp
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
int main() {
std::mutex m;
bool flag = false; // 逻辑检测
int counter = 0;
auto work = [&](int tid) {
for (int i = 0; i < 3; ++i) {
m.lock();
if (flag) {
std::cout << "[ERROR] tid = " << tid << "提前进入临界区" << std::endl;
}
flag = true;
int before = counter;
++counter;
std::cout << "tid = " << tid << " 获得锁, i = " << i
<< " counter: " << before << " -> " << counter
<< std::endl;
flag = false;
// m.unlock();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
};
std::vector<std::thread> ts;
for (int t = 0; t < 3; ++t) {
ts.emplace_back(work, t);
}
for (auto& th : ts) {
th.detach();
}
std::this_thread::sleep_for(std::chrono::seconds(2));
m.unlock(); // 可能导致未定义行为
std::this_thread::sleep_for(std::chrono::seconds(10));
std::cout << "final counter = " << counter << "\n";
return 0;
}
mutex如何工作?
关键点在m变量。
步骤:
m在初始时是unlocktid_0第一次访问m,成功上锁tid_0第二次访问m,已经上锁了,导致自死锁。tid_1第一次访问m,已经上锁了,故线程tid_1挂起,等待m解锁。tid_2第一次访问m,已经上锁了,故线程tid_2挂起,等待m解锁。- 主线程中等待了
2s后,m解锁,此时调度器选择某个线程执行,而后m再次上锁。 - 最后
counter = 2。
关键点:
- 锁的状态是共享的
- 是否阻塞取决于锁的状态
- 阻塞是线程级别的,不是程序级别的
关于锁的
lock/unlock需要在同一线程,否则会导致未定义行为。
tid_0第二次访问m时,按道理来说是不可能再解锁m,自死锁。
RAII锁管理
手写 lock()/unlock() 容易在:
- 异常抛出
- 多分支 return
- 中途
continue/break的路径上漏掉 unlock,导致死锁。
lock_guard/unique_lock的价值是自动释放,避免漏写导致死锁。
std::lock_guard<std::mutex>
cpp
#include <mutex>
using std::lock_guard;
- 构造时
lock() - 析构时
unlock() - 不可手动解锁
- 零额外状态,最小开销
cpp
{
std::lock_guard<std::mutex> g(m);
/**
* 临界区
*/
}
std::unique_lock<std::mutex>
cpp
#include <mutex>
using std::unique_lock;
支持:
- 延迟加锁
- 尝试加锁
- 手动 unlock / re-lock
condition_variable
cpp
std::unique_lock<std::mutex> lk(m, std::defer_lock);
lk.lock();
// 临界区
lk.unlock();
状态管理
std::defer_lock
unique_lock可用
构造时 unique_lock 时不调用 m.lock()
需要显式 lk.lock() 或 lk.try_lock()
cpp
std::mutex m;
void f() {
std::unique_lock<std::mutex> lk(m, std::defer_lock);
// prepare();
lk.lock();
// 临界区
}
std::try_to_lock
unique_lock可用
构造时等价于执行一次 try_lock():
- 成功:
owns_lock()==true - 失败:
owns_lock()==false(不会阻塞)
cpp
std::mutex m;
void f() {
std::unique_lock<std::mutex> lk(m, std::try_to_lock);
if (!lk.owns_lock()) {
// 抢不到锁
return;
}
// 抢到锁:临界区
}
std::adopt_lock
lock_guard,unique_lock可用
cpp
std::lock_guard<std::mutex> g(m, std::adopt_lock);
std::unique_lock<std::mutex> g(m, std::adopt_lock);
等价语义:
- 构造:不调用
m.lock() - 析构:调用
m.unlock()
使用adopt_lock前,当前线程必须已经成功锁住同一个 mutex
cpp
std::mutex m;
void f() {
m.lock(); // 锁住
std::lock_guard<std::mutex> g(m, std::adopt_lock); // 接管
// 临界区
} // 自动 unlock
多锁死锁
死锁条件
- 互斥:锁一次只能被一个线程持有
- 占有且等待:线程持有一把锁,同时等待另一把
- 不可抢占:锁只能由持有线程释放
- 循环等待 :线程之间形成等待环
多把锁死锁,核心就是第 4 条:循环等待。
示例:
cpp
std::mutex m1, m2;
// 线程 A
void f1() {
m1.lock();
std::this_thread::sleep_for(std::chrono::seconds(1));
m2.lock();
// ...
m2.unlock();
m1.unlock();
}
// 线程 B
void f2() {
m2.lock();
std::this_thread::sleep_for(std::chrono::seconds(1));
m1.lock();
// ...
m1.unlock();
m2.unlock();
}
std::lock
cpp
std::lock(m1, m2, ...);
同时锁住所有 mutex。
cpp
#include <mutex>
std::mutex m1, m2;
void f1() {
std::lock(m1, m2);
std::lock_guard<std::mutex> g1(m1, std::adopt_lock);
std::lock_guard<std::mutex> g2(m2, std::adopt_lock);
// 临界区:同时持有 m1 和 m2
} // 自动释放
void f2() {
std::unique_lock<std::mutex> lk1(m1, std::defer_lock);
std::unique_lock<std::mutex> lk2(m2, std::defer_lock);
std::lock(lk1, lk2);
// 临界区
} // 自动释放
std::scoped_lock
C++17
cpp
#include <mutex>
using std::scoped_lock;
std::scoped_lock lk(m1, m2, m3);
- 类似于内部调用
std::lock(m1, m2, m3)+adopt_lock
cpp
std::mutex m1, m2;
void f() {
std::scoped_lock lock(m1, m2); // 一行搞定
// 临界区
}
tips
其实还有其他几个锁,但是都不常用
std::timed_mutex / std::recursive_timed_mutex:支持try_lock_for/try_lock_until,避免无限期阻塞。std::shared_mutex(C++17)+std::shared_lock:读多写少场景,读锁共享、写锁独占。std::atomic:对简单计数/标志位,往往比 mutex 更轻