mutex
1. 概述
在多线程编程中,当多个线程同时访问共享资源时,如果没有适当的同步机制,就会导致数据竞争 (Data Race)和未定义行为 。std::mutex(互斥锁)是 C++11 标准库提供的最基础的线程同步原语,用于保护共享资源,确保同一时刻只有一个线程能够访问被保护的代码区域。
1.1 为什么需要互斥锁?
想象一下银行取款的场景:如果多个客户同时从同一个账户取款,而没有适当的保护机制,账户余额可能会被错误地计算。互斥锁就像银行柜台的"正在服务"指示灯,当一个客户在办理业务时,其他客户必须等待。
在多线程程序中,互斥锁的作用是:
- 防止数据竞争:确保对共享数据的访问是互斥的
- 保证数据一致性:避免多个线程同时修改同一数据导致的不一致状态
- 实现线程同步:协调多个线程的执行顺序
1.2 头文件
使用 std::mutex 需要包含头文件:
cpp
#include <mutex>
2. std::mutex 基础
2.1 类定义
cpp
class mutex
{
public:
mutex() noexcept;
~mutex();
mutex(const mutex&) = delete; // 禁止拷贝
mutex& operator=(const mutex&) = delete; // 禁止赋值
void lock(); // 加锁(阻塞)
bool try_lock(); // 尝试加锁(非阻塞)
void unlock(); // 解锁
// native_handle_type native_handle(); // 获取底层句柄(可选,平台相关)
};
2.2 基本使用
2.2.1 手动加锁和解锁
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx;
int counter = 0;
void IncrementCounter(int times)
{
for (int i = 0; i < times; ++i)
{
mtx.lock(); // 加锁
++counter; // 临界区代码
mtx.unlock(); // 解锁
}
}
int main()
{
std::vector<std::thread> threads;
// 创建 5 个线程,每个线程增加 counter 1000 次
for (int i = 0; i < 5; ++i)
{
threads.emplace_back(IncrementCounter, 1000);
}
// 等待所有线程完成
for (auto& t : threads)
{
t.join();
}
std::cout << "最终 counter 值: " << counter << std::endl;
// 输出: 最终 counter 值: 5000
return 0;
}
注意事项:
-
必须配对使用 :每次
lock()必须对应一次unlock(),否则会导致死锁为什么会死锁?
死锁(Deadlock)是指线程因为无法获取锁而永远阻塞的情况。如果忘记调用
unlock():- 同一线程再次加锁 :如果同一个线程在未解锁的情况下再次调用
lock(),会导致死锁(对于非递归互斥锁) - 其他线程无法获取锁:锁永远不会被释放,其他需要该锁的线程会永远等待
示例:
cpp#include <iostream> #include <thread> #include <mutex> std::mutex mtx; int counter = 0; // ❌ 错误示例:忘记解锁导致死锁 void BadFunction() { mtx.lock(); ++counter; // 忘记调用 mtx.unlock() // 函数返回后,锁永远不会被释放 } void AnotherFunction() { mtx.lock(); // 这里会永远等待,因为 BadFunction() 没有释放锁 ++counter; mtx.unlock(); } int main() { std::thread t1(BadFunction); std::thread t2(AnotherFunction); t1.join(); t2.join(); // 程序会在这里永远阻塞(死锁) return 0; }在这个例子中:
t1线程调用BadFunction(),获取锁后忘记释放t2线程调用AnotherFunction(),尝试获取同一个锁- 由于锁从未被释放,
t2会永远等待,程序陷入死锁
- 同一线程再次加锁 :如果同一个线程在未解锁的情况下再次调用
-
异常安全:如果在临界区代码中抛出异常,可能导致锁无法释放
什么是临界区?
**临界区(Critical Section)**是指被互斥锁保护的代码段,即从
lock()到unlock()之间的代码。在上面的示例中:cppmtx.lock(); // ← 临界区开始 ++counter; // ← 临界区代码(被保护的部分) mtx.unlock(); // ← 临界区结束临界区的特点:
- 互斥性:同一时刻只有一个线程能进入临界区,其他线程必须等待
- 串行化:多个线程对临界区的访问是串行的,有明确的执行顺序
- 可见性:线程在临界区内的修改,在释放锁后对其他线程可见
如果在临界区代码中抛出异常,且没有使用 RAII,锁可能无法释放:
cpp// ❌ 错误示例:异常导致锁无法释放 void UnsafeFunction() { mtx.lock(); // 加锁 SomeFunction(); // 如果这里抛出异常,下面的 unlock() 不会执行 mtx.unlock(); // 锁永远不会被释放,导致死锁 } -
不推荐直接使用 :建议使用 RAII 包装器(如
std::lock_guard)来管理锁
2.3 异常安全问题
直接使用 lock() 和 unlock() 存在异常安全风险:
cpp
// 示例函数(实际使用时替换为真实函数)
void SomeFunction()
{
// 可能抛出异常的操作
}
// ❌ 错误示例:异常不安全
void UnsafeFunction()
{
mtx.lock();
SomeFunction(); // 如果这里抛出异常,锁永远不会被释放
mtx.unlock();
}
// ✅ 正确示例:使用 RAII
void SafeFunction()
{
std::lock_guard<std::mutex> lock(mtx); // 构造时自动加锁
SomeFunction(); // 即使抛出异常,析构时也会自动解锁
// lock 对象析构时自动调用 unlock()
}
3. RAII 锁管理
C++11 提供了两种 RAII(Resource Acquisition Is Initialization)锁管理类,用于自动管理锁的生命周期,确保异常安全。
3.1 std::lock_guard
std::lock_guard 是最简单的锁管理类,构造时加锁,析构时解锁。
3.1.1 类定义
cpp
template<class Mutex>
class lock_guard
{
public:
explicit lock_guard(Mutex& m); // 构造时加锁
lock_guard(Mutex& m, std::adopt_lock_t); // 假设已经持有锁
~lock_guard(); // 析构时解锁
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
};
3.1.2 基本使用
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx;
int counter = 0;
void IncrementCounter(int times)
{
for (int i = 0; i < times; ++i)
{
std::lock_guard<std::mutex> lock(mtx); // 自动加锁
++counter; // 临界区代码
// lock 对象析构时自动解锁
}
}
int main()
{
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i)
{
threads.emplace_back(IncrementCounter, 1000);
}
for (auto& t : threads)
{
t.join();
}
std::cout << "最终 counter 值: " << counter << std::endl;
return 0;
}
3.1.3 adopt_lock 用法
adopt_lock 用于已经持有锁 的情况,它告诉 lock_guard 不要再次加锁,而是假设锁已经被当前线程持有,只需要在析构时解锁即可。
为什么需要 adopt_lock?
在某些场景下,我们需要先手动加锁(比如使用 std::lock 同时锁定多个互斥锁),然后希望使用 RAII 机制自动管理锁的释放。adopt_lock 就是为这种场景设计的。
工作原理
- 不使用
adopt_lock:lock_guard构造时会调用lock(),如果锁已经被持有,会导致死锁 - 使用
adopt_lock:lock_guard构造时不会 调用lock(),只是记录锁的所有权,析构时调用unlock()
使用场景
场景 1:配合 std::lock 使用
这是 adopt_lock 最常见的用法,用于在同时锁定多个互斥锁后,使用 RAII 管理锁的释放:
cpp
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1;
std::mutex mtx2;
void FunctionWithAdoptLock()
{
// 使用 std::lock 同时锁定多个互斥锁(避免死锁)
std::lock(mtx1, mtx2);
// 使用 adopt_lock 告诉 lock_guard 我们已经持有锁
// lock_guard 不会再次加锁,只负责在析构时解锁
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// 临界区代码
// lock1 和 lock2 析构时会自动解锁
}
场景 2:条件加锁
在某些条件下才需要加锁,但希望统一使用 RAII 管理:
cpp
void ConditionalLockFunction(bool needLock)
{
if (needLock)
{
mtx.lock(); // 手动加锁
// 使用 adopt_lock 让 lock_guard 接管锁的管理
std::lock_guard<std::mutex> lock(mtx, std::adopt_lock);
// 临界区代码
// lock 析构时自动解锁
}
else
{
// 不需要锁的操作
}
}
注意事项
- 必须先加锁 :使用
adopt_lock之前,必须确保锁已经被当前线程持有,否则会导致未定义行为 - 不能重复加锁 :如果锁已经被持有,使用
adopt_lock不会再次加锁,这是正确的行为 - 自动解锁 :
lock_guard析构时会自动调用unlock(),无需手动解锁
错误示例
cpp
// ❌ 错误:没有先加锁就使用 adopt_lock
void BadFunction()
{
// 没有调用 mtx.lock()
std::lock_guard<std::mutex> lock(mtx, std::adopt_lock);
// 未定义行为:锁没有被持有,但 lock_guard 认为已经持有
}
// ❌ 错误:使用 adopt_lock 后又手动解锁
void BadFunction2()
{
mtx.lock();
std::lock_guard<std::mutex> lock(mtx, std::adopt_lock);
// 临界区代码
mtx.unlock(); // 错误:lock_guard 析构时还会再次解锁,导致未定义行为
}
// ✅ 正确:先加锁,再使用 adopt_lock,让 lock_guard 自动管理
void GoodFunction()
{
mtx.lock();
std::lock_guard<std::mutex> lock(mtx, std::adopt_lock);
// 临界区代码
// lock 析构时自动解锁,无需手动调用 unlock()
}
完整示例
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx1;
std::mutex mtx2;
int data1 = 0;
int data2 = 0;
void SafeUpdate()
{
// 使用 std::lock 同时锁定两个互斥锁(避免死锁)
std::lock(mtx1, mtx2);
// 使用 adopt_lock 让 lock_guard 接管锁的管理
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// 现在可以安全地修改两个共享变量
++data1;
++data2;
// lock1 和 lock2 析构时会自动解锁
}
int main()
{
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i)
{
threads.emplace_back(SafeUpdate);
}
for (auto& t : threads)
{
t.join();
}
std::cout << "data1: " << data1 << ", data2: " << data2 << std::endl;
return 0;
}
3.2 std::unique_lock
std::unique_lock 比 std::lock_guard 更灵活,支持延迟加锁、条件变量等高级功能。
3.2.1 类定义
cpp
template<class Mutex>
class unique_lock
{
public:
unique_lock() noexcept; // 默认构造(不持有锁)
explicit unique_lock(Mutex& m); // 构造时加锁
unique_lock(Mutex& m, std::defer_lock_t); // 延迟加锁
unique_lock(Mutex& m, std::try_to_lock_t); // 尝试加锁
unique_lock(Mutex& m, std::adopt_lock_t); // 假设已经持有锁
~unique_lock(); // 析构时解锁
void lock(); // 加锁
bool try_lock(); // 尝试加锁
void unlock(); // 解锁
bool owns_lock() const; // 检查是否持有锁
Mutex* release(); // 释放锁的所有权(不解锁)
};
3.2.2 基本使用
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx;
int counter = 0;
void IncrementCounter(int times)
{
for (int i = 0; i < times; ++i)
{
std::unique_lock<std::mutex> lock(mtx); // 自动加锁
++counter;
// lock 析构时自动解锁
}
}
int main()
{
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i)
{
threads.emplace_back(IncrementCounter, 1000);
}
for (auto& t : threads)
{
t.join();
}
std::cout << "最终 counter 值: " << counter << std::endl;
return 0;
}
3.2.3 延迟加锁(defer_lock)
defer_lock 用于延迟加锁,构造 unique_lock 时不立即加锁,而是稍后根据需要手动加锁。这提供了更灵活的锁控制。
工作原理
- 不使用
defer_lock:unique_lock构造时立即调用lock()加锁 - 使用
defer_lock:unique_lock构造时不调用lock(),锁对象处于"未锁定"状态,可以稍后调用lock()加锁
使用场景
场景 1:需要先执行非临界区代码,再进入临界区
cpp
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int counter = 0;
void FunctionWithDeferLock()
{
// 创建 unique_lock,但不立即加锁
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// 做一些不需要锁的准备工作(减少锁的持有时间)
int localData = 100;
// 进行一些计算...
// 现在才加锁,进入临界区
lock.lock();
counter += localData; // 临界区代码
lock.unlock(); // 可以手动解锁,释放锁
// 做一些不需要锁的后处理工作
// 清理工作...
// 如果需要,可以再次加锁
lock.lock();
// 更多临界区代码
// lock 析构时自动解锁
}
场景 2:配合 std::lock 同时锁定多个互斥锁
这是 defer_lock 最常见的用法,用于避免死锁:
cpp
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1;
std::mutex mtx2;
void SafeFunction()
{
// 创建两个 unique_lock,但不立即加锁
std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
// 使用 std::lock 同时锁定两个锁(避免死锁)
std::lock(lock1, lock2);
// 临界区代码
// lock1 和 lock2 析构时自动解锁
}
场景 3:条件加锁
根据条件决定是否需要加锁:
cpp
void ConditionalLockFunction(bool needLock)
{
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
if (needLock)
{
lock.lock(); // 只在需要时才加锁
// 临界区代码
}
else
{
// 不需要锁的操作
}
// 如果持有锁,析构时自动解锁
}
注意事项
- 必须手动加锁 :使用
defer_lock后,必须手动调用lock()才能进入临界区 - 可以多次加锁解锁 :
unique_lock支持多次调用lock()和unlock() - 检查锁状态 :可以使用
owns_lock()检查当前是否持有锁 - 异常安全 :即使忘记手动加锁,
unique_lock析构时也不会出错(因为没有持有锁)
错误示例
cpp
// ❌ 错误:使用 defer_lock 后忘记加锁
void BadFunction()
{
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// 忘记调用 lock.lock()
// 直接访问共享资源,没有锁保护,导致数据竞争
counter++;
}
// ✅ 正确:使用 defer_lock 后记得加锁
void GoodFunction()
{
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
lock.lock(); // 必须手动加锁
counter++;
// lock 析构时自动解锁
}
完整示例
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx1;
std::mutex mtx2;
int data1 = 0;
int data2 = 0;
void SafeUpdate()
{
// 使用 defer_lock 创建锁对象,但不立即加锁
std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
// 做一些不需要锁的准备工作
int localValue1 = 10;
int localValue2 = 20;
// 使用 std::lock 同时锁定两个锁(避免死锁)
std::lock(lock1, lock2);
// 现在可以安全地修改共享数据
data1 += localValue1;
data2 += localValue2;
// lock1 和 lock2 析构时自动解锁
}
int main()
{
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i)
{
threads.emplace_back(SafeUpdate);
}
for (auto& t : threads)
{
t.join();
}
std::cout << "data1: " << data1 << ", data2: " << data2 << std::endl;
return 0;
}
3.2.4 尝试加锁(try_to_lock)
try_to_lock 用于非阻塞地尝试加锁,如果锁不可用,立即返回而不等待。这对于需要避免阻塞的场景非常有用。
工作原理
- 不使用
try_to_lock:unique_lock构造时调用lock(),如果锁不可用会阻塞等待 - 使用
try_to_lock:unique_lock构造时调用try_lock(),如果锁不可用立即返回,不阻塞
使用场景
场景 1:避免阻塞,执行替代操作
当无法获取锁时,执行其他操作而不是等待:
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mtx;
int counter = 0;
void FunctionWithTryLock()
{
// 尝试获取锁,不阻塞
std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
if (lock.owns_lock()) // 检查是否成功获取锁
{
// 成功获取锁,执行临界区代码
++counter;
std::cout << "成功获取锁,更新 counter" << std::endl;
}
else
{
// 未能获取锁,执行其他操作
std::cout << "无法获取锁,执行其他操作" << std::endl;
// 可以执行一些不需要锁的操作
}
// lock 析构时,如果持有锁会自动解锁
}
场景 2:实现超时机制
结合循环和延迟,实现类似超时的效果:
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mtx;
void TryLockWithTimeout()
{
const int maxAttempts = 10;
const auto delay = std::chrono::milliseconds(100);
for (int i = 0; i < maxAttempts; ++i)
{
std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
if (lock.owns_lock())
{
// 成功获取锁
std::cout << "成功获取锁,执行操作" << std::endl;
return;
}
// 未能获取锁,等待一段时间后重试
std::this_thread::sleep_for(delay);
}
// 超时,执行替代操作
std::cout << "超时,无法获取锁" << std::endl;
}
场景 3:避免死锁
在需要多个锁时,如果无法获取所有锁,立即放弃:
cpp
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1;
std::mutex mtx2;
void TryLockMultiple()
{
std::unique_lock<std::mutex> lock1(mtx1, std::try_to_lock);
if (!lock1.owns_lock())
{
// 无法获取第一个锁,立即返回
std::cout << "无法获取 mtx1,放弃操作" << std::endl;
return;
}
std::unique_lock<std::mutex> lock2(mtx2, std::try_to_lock);
if (!lock2.owns_lock())
{
// 无法获取第二个锁,释放第一个锁并返回
std::cout << "无法获取 mtx2,放弃操作" << std::endl;
return;
}
// 成功获取所有锁,执行操作
std::cout << "成功获取所有锁,执行操作" << std::endl;
}
场景 4:优先级处理
高优先级任务可以尝试获取锁,如果失败则跳过:
cpp
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
void HighPriorityTask()
{
std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
if (lock.owns_lock())
{
// 成功获取锁,执行高优先级任务
std::cout << "执行高优先级任务" << std::endl;
}
else
{
// 无法获取锁,跳过本次执行
std::cout << "资源被占用,跳过本次执行" << std::endl;
}
}
注意事项
- 必须检查锁状态 :使用
try_to_lock后,必须使用owns_lock()检查是否成功获取锁 - 非阻塞 :
try_to_lock不会阻塞,立即返回 - 可能失败:锁可能被其他线程持有,尝试加锁可能失败
- 异常安全 :即使没有获取锁,
unique_lock析构时也不会出错
错误示例
cpp
// ❌ 错误:没有检查锁状态就使用
void BadFunction()
{
std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
// 没有检查 owns_lock()
counter++; // 如果锁获取失败,这里没有锁保护,导致数据竞争
}
// ✅ 正确:检查锁状态后再使用
void GoodFunction()
{
std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
if (lock.owns_lock()) // 必须检查
{
counter++; // 只有在持有锁时才访问共享资源
}
}
完整示例
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <chrono>
std::mutex mtx;
int counter = 0;
void WorkerThread(int threadId)
{
for (int i = 0; i < 10; ++i)
{
// 尝试获取锁
std::unique_lock<std::mutex> lock(mtx, std::try_to_lock);
if (lock.owns_lock())
{
// 成功获取锁,更新共享资源
++counter;
std::cout << "线程 " << threadId << " 更新 counter: " << counter << std::endl;
// 模拟一些工作
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
else
{
// 无法获取锁,执行其他操作
std::cout << "线程 " << threadId << " 无法获取锁,跳过本次操作" << std::endl;
// 等待一段时间后重试
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
}
int main()
{
std::vector<std::thread> threads;
// 创建 3 个线程
for (int i = 0; i < 3; ++i)
{
threads.emplace_back(WorkerThread, i + 1);
}
for (auto& t : threads)
{
t.join();
}
std::cout << "最终 counter: " << counter << std::endl;
return 0;
}
try_to_lock vs try_lock()
std::unique_lock 的 try_to_lock 构造函数内部调用的是互斥锁的 try_lock() 方法:
cpp
// 这两种方式是等价的
std::unique_lock<std::mutex> lock1(mtx, std::try_to_lock);
// 等价于
std::unique_lock<std::mutex> lock2(mtx, std::defer_lock);
if (lock2.try_lock())
{
// 成功获取锁
}
3.2.5 lock_guard vs unique_lock
| 特性 | lock_guard | unique_lock |
|---|---|---|
| 构造时加锁 | 是 | 可选 |
| 手动解锁 | 否 | 是 |
| 延迟加锁 | 否 | 是 |
| 尝试加锁 | 否 | 是 |
| 条件变量 | 不支持 | 支持 |
| 性能开销 | 低 | 稍高 |
| 灵活性 | 低 | 高 |
为什么有这些差异?
1. 构造时加锁:为什么 lock_guard 是"是",unique_lock 是"可选"?
原因 :lock_guard 设计为简单、轻量级的锁管理工具,只提供最基本的 RAII 功能。unique_lock 设计为更灵活的锁管理工具,支持多种加锁策略。
对比示例:
cpp
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
// lock_guard:构造时必须加锁
void FunctionWithLockGuard()
{
// ✅ 正确:构造时自动加锁
std::lock_guard<std::mutex> lock(mtx);
// 临界区代码
// ❌ 错误:lock_guard 不支持延迟加锁
// std::lock_guard<std::mutex> lock(mtx, std::defer_lock); // 编译错误
}
// unique_lock:构造时可以延迟加锁
void FunctionWithUniqueLock()
{
// ✅ 方式1:构造时自动加锁
std::unique_lock<std::mutex> lock1(mtx);
// ✅ 方式2:延迟加锁
std::unique_lock<std::mutex> lock2(mtx, std::defer_lock);
lock2.lock(); // 稍后手动加锁
}
2. 手动解锁:为什么 lock_guard 是"否",unique_lock 是"是"?
原因 :lock_guard 设计为"获取即锁定,析构即解锁"的简单模式,不支持手动控制。unique_lock 提供更细粒度的控制,允许手动解锁以减小锁的持有时间。
对比示例:
cpp
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int counter = 0;
// lock_guard:不支持手动解锁
void FunctionWithLockGuard()
{
std::lock_guard<std::mutex> lock(mtx);
// 临界区代码
++counter;
// ❌ 错误:lock_guard 没有 unlock() 方法
// lock.unlock(); // 编译错误
// 只能等待 lock 析构时自动解锁
}
// unique_lock:支持手动解锁(无论是否使用 defer_lock)
void FunctionWithUniqueLock()
{
// 方式1:不使用 defer_lock,构造时自动加锁
std::unique_lock<std::mutex> lock(mtx); // 构造时自动加锁
// 临界区代码
++counter;
// ✅ 正确:即使没有使用 defer_lock,也可以手动解锁
// unique_lock 无论构造时是否加锁,都支持 unlock() 方法
lock.unlock(); // 手动解锁,此时 lock 不再持有锁
// 做一些不需要锁的操作(锁已释放,其他线程可以获取)
// DoSomeWork();
// 如果需要,可以再次加锁
lock.lock(); // 再次加锁
// 更多临界区代码
// lock 析构时自动解锁(如果持有锁的话)
}
// 对比:使用 defer_lock 的情况
void FunctionWithDeferLock()
{
// 方式2:使用 defer_lock,构造时不加锁
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 构造时**不**加锁
// 做一些不需要锁的操作
// DoSomeWork();
lock.lock(); // 手动加锁
// 临界区代码
++counter;
lock.unlock(); // 手动解锁(同样支持)
// 如果需要,可以再次加锁
lock.lock();
// 更多临界区代码
// lock 析构时自动解锁(如果持有锁的话)
}
// 总结:unique_lock 的 unlock() 方法不依赖于 defer_lock
// - 不使用 defer_lock:构造时自动加锁,可以手动解锁
// - 使用 defer_lock:构造时不加锁,需要手动加锁,也可以手动解锁
// 无论哪种方式,都支持手动解锁
3. 条件变量:为什么 lock_guard 不支持,unique_lock 支持?
原因 :条件变量(std::condition_variable)的 wait() 方法需要能够临时释放锁 ,等待条件满足后再重新获取锁。lock_guard 不支持手动解锁,无法满足这个需求。unique_lock 支持手动解锁和重新加锁,可以与条件变量配合使用。
对比示例:
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> dataQueue;
bool ready = false;
// ❌ 错误:lock_guard 不能与条件变量配合使用
void BadFunction()
{
std::lock_guard<std::mutex> lock(mtx);
// ❌ 错误:condition_variable::wait() 需要能够释放和重新获取锁
// lock_guard 不支持 unlock(),无法满足条件变量的需求
// cv.wait(lock); // 编译错误:lock_guard 没有 unlock() 方法
}
// ✅ 正确:unique_lock 可以与条件变量配合使用
void GoodFunction()
{
std::unique_lock<std::mutex> lock(mtx);
// 等待条件满足
// wait() 会临时释放锁,等待条件满足后重新获取锁
cv.wait(lock, []() { return ready; });
// 条件满足后,继续执行(此时已重新持有锁)
// 处理数据...
}
完整示例:生产者-消费者模式
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> queue;
bool finished = false;
// 生产者
void Producer()
{
for (int i = 0; i < 10; ++i)
{
{
std::lock_guard<std::mutex> lock(mtx);
queue.push(i);
}
cv.notify_one(); // 通知消费者
}
{
std::lock_guard<std::mutex> lock(mtx);
finished = true;
}
cv.notify_one(); // 通知消费者结束
}
// 消费者
void Consumer()
{
while (true)
{
std::unique_lock<std::mutex> lock(mtx);
// 等待队列非空或生产结束(wait 会临时释放锁,被唤醒后重新获取锁)
cv.wait(lock, []() { return !queue.empty() || finished; });
// 如果生产结束且队列为空,退出
if (finished && queue.empty())
{
break;
}
// 处理一个数据
if (!queue.empty())
{
int value = queue.front();
queue.pop();
lock.unlock(); // 解锁后再处理,减小锁的持有时间
std::cout << "消费: " << value << std::endl;
// 注意:已经手动解锁,lock 析构时不会再解锁(这是正确的行为)
}
}
}
int main()
{
std::thread producer(Producer);
std::thread consumer(Consumer);
producer.join();
consumer.join();
return 0;
}
代码说明:
-
同步机制:
- 互斥锁保护共享数据(队列和
finished标志) - 条件变量实现线程间通知
wait()在等待时释放锁,被唤醒后重新获取锁
- 互斥锁保护共享数据(队列和
-
生产者:
- 在锁保护下推入数据
- 在锁外通知消费者(避免不必要的阻塞)
- 设置结束标志后通知消费者
-
消费者:
- 使用
unique_lock配合条件变量(lock_guard不支持) - 等待队列非空或生产结束
- 循环处理队列中的所有数据
- 处理时解锁,减小锁的持有时间
- 使用
总结:
- lock_guard:简单、高效,适合大多数简单场景
- unique_lock:灵活、功能强大,适合需要精细控制或与条件变量配合的场景
选择建议:
- 简单场景 :使用
std::lock_guard - 需要条件变量 :使用
std::unique_lock - 需要手动控制锁 :使用
std::unique_lock - 性能敏感场景 :优先使用
std::lock_guard
4. 多锁管理
4.1 std::lock
std::lock 是一个函数模板,可以同时锁定多个互斥锁,避免死锁。
4.1.1 函数签名
cpp
template<class Lockable1, class Lockable2, class... LockableN>
void lock(Lockable1& l1, Lockable2& l2, LockableN&... ln);
4.1.2 避免死锁
当需要同时持有多个锁时,如果加锁顺序不一致,可能导致死锁:
cpp
// ❌ 错误示例:可能导致死锁
void Thread1()
{
mtx1.lock();
mtx2.lock();
// 临界区代码
mtx2.unlock();
mtx1.unlock();
}
void Thread2()
{
mtx2.lock(); // 与 Thread1 的加锁顺序相反
mtx1.lock(); // 可能导致死锁
// 临界区代码
mtx1.unlock();
mtx2.unlock();
}
使用 std::lock 可以避免死锁:
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx1;
std::mutex mtx2;
int account1 = 1000;
int account2 = 1000;
// 转账函数:需要同时锁定两个账户
void Transfer(int amount)
{
// std::lock 使用死锁避免算法,同时锁定多个互斥锁
std::lock(mtx1, mtx2);
// 使用 adopt_lock 告诉 lock_guard 我们已经持有锁
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// 临界区:同时修改两个账户
account1 -= amount;
account2 += amount;
}
// 反向转账:加锁顺序不同
void ReverseTransfer(int amount)
{
// 即使参数顺序与 Transfer 不同,std::lock 也能避免死锁
std::lock(mtx2, mtx1); // 顺序相反,但不会死锁
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
// 临界区:反向转账
account2 -= amount;
account1 += amount;
}
int main()
{
std::vector<std::thread> threads;
// 创建多个线程,同时进行转账和反向转账
for (int i = 0; i < 5; ++i)
{
threads.emplace_back(Transfer, 100);
threads.emplace_back(ReverseTransfer, 50);
}
for (auto& t : threads)
{
t.join();
}
std::cout << "最终余额:账户1 = " << account1
<< ", 账户2 = " << account2 << std::endl;
return 0;
}
这个案例说明了什么?
这个案例主要说明:即使不同函数中加锁顺序不同,std::lock 也能避免死锁。
核心要点:
-
问题场景 :
Transfer使用std::lock(mtx1, mtx2),ReverseTransfer使用std::lock(mtx2, mtx1),加锁顺序相反 -
如果手动加锁会怎样?
cpp// ❌ 手动加锁:可能导致死锁 void Transfer(int amount) { mtx1.lock(); // 线程1先锁 mtx1 mtx2.lock(); // 线程1等待 mtx2 // ... } void ReverseTransfer(int amount) { mtx2.lock(); // 线程2先锁 mtx2 mtx1.lock(); // 线程2等待 mtx1(死锁!) // ... } -
std::lock 如何避免死锁?
std::lock使用死锁避免算法,内部会尝试获取所有锁- 如果无法同时获取所有锁,会释放已获取的锁并重试
- 这样确保了要么同时获取所有锁,要么一个都不获取
-
实际意义 :使用
std::lock后,开发者不需要关心加锁顺序,可以安全地同时获取多个锁
std::lock 的工作原理:
std::lock 使用死锁避免算法,大致流程如下:
- 尝试按顺序锁定所有互斥锁
- 如果某个锁无法获取,释放所有已获取的锁
- 等待一小段时间后重试
- 重复直到成功获取所有锁
这样确保了要么同时获取所有锁,要么一个都不获取,避免了死锁。
4.1.3 使用 unique_lock 的简化写法
cpp
void SimplifiedSafeFunction()
{
std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
// 同时锁定两个锁
std::lock(lock1, lock2);
// 临界区代码
// lock1 和 lock2 析构时自动解锁
}
4.2 std::try_lock
std::try_lock 尝试同时锁定多个互斥锁,如果任何一个锁无法获取,则释放所有已获取的锁并返回。
4.2.1 函数签名
cpp
template<class Lockable1, class Lockable2, class... LockableN>
int try_lock(Lockable1& l1, Lockable2& l2, LockableN&... ln);
返回值:
- 成功:返回
-1 - 失败:返回第一个无法获取的锁的索引(从 0 开始)
4.2.2 使用示例
cpp
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1;
std::mutex mtx2;
void TryLockFunction()
{
int result = std::try_lock(mtx1, mtx2);
if (result == -1)
{
// 成功获取所有锁
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
// 临界区代码
std::cout << "成功获取所有锁" << std::endl;
}
else
{
// 未能获取所有锁
std::cout << "无法获取锁,索引: " << result << std::endl;
}
}
5. 其他类型的互斥锁
5.1 std::recursive_mutex
std::recursive_mutex 是递归互斥锁,允许同一线程多次加锁。
5.1.1 使用场景
当函数可能被递归调用,或者需要多次加锁时,使用递归互斥锁:
cpp
#include <iostream>
#include <thread>
#include <mutex>
std::recursive_mutex rmtx;
void RecursiveFunction(int depth)
{
if (depth <= 0)
{
return;
}
std::lock_guard<std::recursive_mutex> lock(rmtx); // 同一线程可以多次加锁
std::cout << "深度: " << depth << std::endl;
RecursiveFunction(depth - 1); // 递归调用,再次尝试加锁(成功)
// lock 析构时解锁
}
int main()
{
std::thread t(RecursiveFunction, 5);
t.join();
return 0;
}
注意 :如果使用普通的 std::mutex,递归调用会导致死锁。
为什么会死锁?
普通 std::mutex 是非递归互斥锁,同一个线程不能多次加锁。如果线程已经持有锁,再次尝试加锁会导致死锁。
5.2 std::timed_mutex
std::timed_mutex 是带超时的互斥锁,支持尝试加锁和超时加锁。
5.2.1 类定义
cpp
class timed_mutex
{
public:
void lock();
bool try_lock();
template<class Rep, class Period>
bool try_lock_for(const std::chrono::duration<Rep, Period>& timeout);
template<class Clock, class Duration>
bool try_lock_until(const std::chrono::time_point<Clock, Duration>& timeout);
void unlock();
};
5.2.2 使用示例
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::timed_mutex tmtx;
void TimedLockFunction()
{
// 尝试在 1 秒内获取锁
if (tmtx.try_lock_for(std::chrono::seconds(1)))
{
std::lock_guard<std::timed_mutex> lock(tmtx, std::adopt_lock);
std::cout << "成功获取锁" << std::endl;
// 模拟一些工作
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
}
else
{
std::cout << "超时,未能获取锁" << std::endl;
}
}
int main()
{
std::thread t1(TimedLockFunction);
std::thread t2(TimedLockFunction);
t1.join();
t2.join();
return 0;
}
5.3 std::recursive_timed_mutex
std::recursive_timed_mutex 结合了递归互斥锁和超时互斥锁的特性。
6. 实际应用示例
6.1 线程安全的计数器
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
class ThreadSafeCounter
{
public:
void Increment()
{
std::lock_guard<std::mutex> lock(mtx);
++value;
}
void Decrement()
{
std::lock_guard<std::mutex> lock(mtx);
--value;
}
int GetValue() const
{
std::lock_guard<std::mutex> lock(mtx);
return value;
}
private:
mutable std::mutex mtx; // mutable 允许在 const 函数中加锁
int value = 0;
};
int main()
{
ThreadSafeCounter counter;
std::vector<std::thread> threads;
// 创建多个线程同时操作计数器
for (int i = 0; i < 10; ++i)
{
if (i % 2 == 0)
{
threads.emplace_back([&counter]()
{
for (int j = 0; j < 1000; ++j)
{
counter.Increment();
}
});
}
else
{
threads.emplace_back([&counter]()
{
for (int j = 0; j < 1000; ++j)
{
counter.Decrement();
}
});
}
}
for (auto& t : threads)
{
t.join();
}
std::cout << "最终值: " << counter.GetValue() << std::endl;
return 0;
}
6.2 线程安全的队列(简化版)
cpp
#include <queue>
#include <mutex>
#include <thread>
#include <chrono>
#include <iostream>
template<typename T>
class ThreadSafeQueue
{
public:
void Push(const T& item)
{
std::lock_guard<std::mutex> lock(mtx);
queue.push(item);
}
bool Pop(T& item)
{
std::lock_guard<std::mutex> lock(mtx);
if (queue.empty())
{
return false;
}
item = queue.front();
queue.pop();
return true;
}
bool Empty() const
{
std::lock_guard<std::mutex> lock(mtx);
return queue.empty();
}
private:
mutable std::mutex mtx;
std::queue<T> queue;
};
int main()
{
ThreadSafeQueue<int> queue;
// 生产者线程
std::thread producer([&queue]()
{
for (int i = 0; i < 10; ++i)
{
queue.Push(i);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
});
// 消费者线程
std::thread consumer([&queue]()
{
int value;
// 注意:这个实现存在竞态条件,实际应用中应该使用条件变量
// 这里仅作为简化示例,展示基本的线程安全队列操作
while (queue.Pop(value))
{
std::cout << "消费: " << value << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
});
producer.join();
consumer.join();
return 0;
}
7. 常见错误和注意事项
7.1 死锁
死锁是指两个或多个线程相互等待对方释放锁,导致程序无法继续执行。
7.1.1 死锁示例
cpp
// ❌ 错误示例:死锁
std::mutex mtx1;
std::mutex mtx2;
void Thread1()
{
mtx1.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟工作
mtx2.lock(); // 等待 Thread2 释放 mtx2
// 临界区代码
mtx2.unlock();
mtx1.unlock();
}
void Thread2()
{
mtx2.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟工作
mtx1.lock(); // 等待 Thread1 释放 mtx1(死锁!)
// 临界区代码
mtx1.unlock();
mtx2.unlock();
}
7.1.2 避免死锁的方法
- 统一加锁顺序:所有线程按照相同的顺序加锁
- 使用 std::lock:同时锁定多个互斥锁
- 避免嵌套锁:尽量减少锁的嵌套
- 使用超时锁 :使用
std::timed_mutex设置超时
7.2 锁的粒度
锁的粒度应该尽可能小,只保护必要的代码:
cpp
// ❌ 错误示例:锁的粒度过大
void BadFunction()
{
std::lock_guard<std::mutex> lock(mtx);
// DoWork1(); // 不需要锁的操作
// DoWork2(); // 不需要锁的操作
// DoCriticalWork(); // 需要锁的操作
// DoWork3(); // 不需要锁的操作
}
// ✅ 正确示例:锁的粒度小
void GoodFunction()
{
// DoWork1(); // 不需要锁的操作
{
std::lock_guard<std::mutex> lock(mtx);
// DoCriticalWork(); // 只保护需要锁的操作
}
// DoWork2(); // 不需要锁的操作
// DoWork3(); // 不需要锁的操作
}
7.3 不要在持有锁时调用用户代码
cpp
#include <functional>
#include <mutex>
// ❌ 错误示例:在持有锁时调用用户代码
void BadFunction(std::function<void()> callback)
{
std::lock_guard<std::mutex> lock(mtx);
callback(); // 用户代码可能持有其他锁,导致死锁
}
// ✅ 正确示例:先调用用户代码,再加锁
void GoodFunction(std::function<void()> callback)
{
callback(); // 先执行用户代码
std::lock_guard<std::mutex> lock(mtx);
// 临界区代码
}
7.4 不要忘记解锁
虽然使用 RAII 可以自动解锁,但在某些情况下仍需注意:
cpp
// ❌ 错误示例:忘记解锁
void BadFunction()
{
mtx.lock();
// DoWork();
// 忘记 unlock(),导致死锁
}
// ✅ 正确示例:使用 RAII
void GoodFunction()
{
std::lock_guard<std::mutex> lock(mtx);
// DoWork();
// 自动解锁
}
8. 性能考虑
8.1 锁的开销
锁操作有一定的开销:
- 系统调用开销:加锁和解锁可能涉及系统调用
- 上下文切换:等待锁的线程可能被阻塞
- 缓存失效:锁的竞争可能导致 CPU 缓存失效
8.2 减少锁竞争
- 减小锁的粒度:只保护必要的代码
- 使用无锁数据结构:对于简单操作,考虑使用原子操作
- 读写分离 :使用读写锁(
std::shared_mutex,C++17) - 减少共享数据:尽量减少线程间共享的数据
8.3 锁的性能对比
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
#include <vector>
std::mutex mtx;
int counter = 0;
void IncrementWithLock(int times)
{
for (int i = 0; i < times; ++i)
{
std::lock_guard<std::mutex> lock(mtx);
++counter;
}
}
int main()
{
const int threadCount = 4;
const int incrementPerThread = 1000000;
auto start = std::chrono::high_resolution_clock::now();
std::vector<std::thread> threads;
for (int i = 0; i < threadCount; ++i)
{
threads.emplace_back(IncrementWithLock, incrementPerThread);
}
for (auto& t : threads)
{
t.join();
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "耗时: " << duration.count() << " 毫秒" << std::endl;
std::cout << "最终值: " << counter << std::endl;
return 0;
}
9. 总结
std::mutex 是 C++11 提供的线程同步基础工具,正确使用可以保证多线程程序的数据安全。关键要点:
- 优先使用 RAII :使用
std::lock_guard或std::unique_lock自动管理锁 - 避免死锁 :统一加锁顺序,或使用
std::lock同时锁定多个锁 - 减小锁粒度:只保护必要的代码,减少锁竞争
- 异常安全:使用 RAII 确保异常时也能正确释放锁
- 性能考虑:在保证正确性的前提下,尽量减少锁的使用
记住:正确性永远比性能更重要。只有在确保程序正确运行后,才应该考虑性能优化。