C++ 中的 std::mutex
与多线程同步
在多线程编程中,互斥锁(Mutex) 是一种同步机制,用于保护共享资源(如变量、数据结构)免受数据竞争(Data Race)的影响。C++ 标准库中的 std::mutex
提供了基本的互斥功能,常与 std::lock_guard
或 std::unique_lock
配合使用,以实现线程安全的资源访问。
1. std::mutex
的基本概念
- 作用:确保在同一时刻只有一个线程可以访问共享资源。
- 头文件:
#include <mutex>
- 常用方法:
lock()
:锁定互斥锁,若已被锁定则阻塞等待。unlock()
:释放互斥锁。try_lock()
:尝试锁定互斥锁,若失败则立即返回(不阻塞)。
2. 示例:不使用互斥锁导致数据竞争
场景:多个线程对共享计数器进行递增操作。
#include <iostream>
#include <thread>
#include <vector>
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
++counter; // 非原子操作,存在数据竞争
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
输出(每次运行结果可能不同):
Final counter value: 789123 // 错误值(期望 1,000,000)
问题分析:
++counter
不是原子操作,多个线程同时修改counter
导致数据竞争。- 结果不可预测,可能远小于预期值。
3. 使用 std::mutex
和 std::lock_guard
修复数据竞争
改进方案:通过互斥锁保护共享资源。
#include <iostream>
#include <thread>
#include <vector>
#include <mutex> // 引入 mutex
int counter = 0;
std::mutex mtx; // 定义互斥锁
void increment() {
for (int i = 0; i < 100000; ++i) {
mtx.lock(); // 加锁
++counter; // 安全访问共享资源
mtx.unlock(); // 解锁
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
输出:
Final counter value: 1000000
关键点:
- 互斥锁保护临界区:
mtx.lock()
和mtx.unlock()
之间的代码为临界区,确保一次只有一个线程执行。 - 避免死锁:必须成对调用
lock()
和unlock()
,否则可能导致死锁。
4. 使用 std::lock_guard
自动管理锁的生命周期
改进方案:使用 RAII(Resource Acquisition Is Initialization) 模式自动加锁/解锁。
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
int counter = 0;
std::mutex mtx;
void increment() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // 构造时加锁,析构时自动解锁
++counter; // 安全访问共享资源
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
输出:
Final counter value: 1000000
优势:
- 自动解锁:
lock_guard
在作用域结束时自动调用unlock()
,避免手动管理锁。 - 异常安全:即使发生异常,
lock_guard
也会确保解锁。
5. std::unique_lock
:更灵活的锁管理
使用场景:需要延迟锁定、条件变量或动态锁定策略。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int shared_data = 0;
void access_data() {
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 不立即加锁
// 做一些不需要锁的操作...
lock.lock(); // 显式加锁
shared_data = 42;
std::cout << "Data updated: " << shared_data << std::endl;
// lock 在作用域结束时自动解锁
}
int main() {
std::thread t(access_data);
t.join();
return 0;
}
输出:
Data updated: 42
特点:
- 延迟锁定:使用
std::defer_lock
参数延迟加锁。 - 支持移动:可以将锁的所有权转移到其他线程。
- 配合条件变量:常用于
std::condition_variable
的等待操作。
6. 互斥锁的最佳实践
原则 | 说明 |
---|---|
保护共享资源 | 所有对共享资源的访问都必须通过互斥锁保护。 |
最小化临界区 | 锁定时间越短越好,避免阻塞其他线程。 |
避免死锁 | 按固定顺序加锁,不嵌套加锁。 |
使用 RAII 风格 | 优先使用 lock_guard 或 unique_lock ,避免手动调用 lock() /unlock() 。 |
避免递归加锁 | 默认 std::mutex 不支持递归加锁,多次调用 lock() 会导致死锁。 |
7. 总结
- 互斥锁(
std::mutex
) 是 C++ 多线程编程中保护共享资源的核心工具。 std::lock_guard
提供自动加锁/解锁,适合大多数同步场景。std::unique_lock
提供更灵活的锁管理,适用于复杂同步需求(如条件变量)。- 关键点:确保所有共享资源访问都通过互斥锁保护,避免数据竞争和死锁。
通过合理使用互斥锁,可以编写出高效、安全的多线程程序。