C++ 多线程同步:互斥锁 mutex 实战
在多线程环境下,多个线程同时读写共享资源时,很容易出现数据竞争。比如两个线程同时对一个全局变量做自增操作,最终结果往往小于预期。这是因为 count++ 并非原子操作,指令被交错执行了。解决这类问题的核心就是线程同步。
为什么需要互斥锁
C++11 引入了 <mutex> 头文件,提供了 std::mutex 类。最基础的接口包括 lock() 获取锁,unlock() 释放锁,以及 try_lock() 尝试获取但不阻塞。
手动调用 lock() 和 unlock() 有个隐患:如果中间抛出异常,unlock() 可能不会被执行,导致死锁。所以实际开发中,更推荐基于 RAII 机制的 std::lock_guard。它会在构造时自动加锁,析构时自动解锁,即使发生异常也能保证锁被释放。
实战:修复数据竞争
来看个例子。之前那个计数程序,加上互斥锁后逻辑就清晰了。
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int count = 0;
mutex mtx; // 定义全局互斥锁
void increment() {
for (int i = 0; i < 100000; ++i) {
lock_guard<mutex> lock(mtx); // 自动加锁
count++; // 临界区代码,此时只有一个线程能执行
}
// lock_guard 析构,自动解锁
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
cout << "最终 count 值:" << count << endl;
return 0;
}
加上锁之后,count 的值就能稳定等于 200000。互斥锁保证了同一时刻只有一个线程能进入临界区。
警惕死锁
死锁是另一个常见坑。当多个线程互相持有对方需要的锁,且都在等待对方释放时,程序就会卡死。产生死锁通常需要满足四个条件:互斥、请求与保持、不可剥夺、循环等待。
下面这个场景就容易死锁:线程 1 拿了锁 A 等锁 B,线程 2 拿了锁 B 等锁 A。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
using namespace std;
mutex mtx1, mtx2;
void thread1() {
mtx1.lock();
this_thread::sleep_for(chrono::milliseconds(100));
mtx2.lock();
cout << "thread1 执行完毕" << endl;
mtx2.unlock();
mtx1.unlock();
}
void thread2() {
mtx2.lock();
this_thread::sleep_for(chrono::milliseconds(100));
mtx1.lock();
cout << "thread2 执行完毕" << endl;
mtx1.unlock();
mtx2.unlock();
}
int main() {
thread t1(thread1);
thread t2(thread2);
t1.join();
t2.join();
return 0;
}
运行这段代码,两个线程会互相等待,陷入死锁状态。
如何避免死锁
主要有几种办法。第一是固定锁的获取顺序,所有线程都按同一个顺序拿锁,比如先 mtx1 再 mtx2。第二是使用 std::lock 一次性获取多个锁,它内部会处理顺序问题。第三是用带超时的锁尝试,超时了就放弃,不一直堵着。
案例:多线程售票系统
模拟一个多窗口售票场景,保证票数不会卖成负数或重复卖。
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
using namespace std;
int tickets = 100; // 总票数
mutex mtx; // 售票函数
void sell_tickets(int window_id) {
while (true) {
lock_guard<mutex> lock(mtx);
if (tickets > 0) {
cout << "窗口" << window_id << "售出第" << tickets << "张票" << endl;
tickets--;
this_thread::sleep_for(chrono::milliseconds(50)); // 模拟售票耗时
} else {
break;
}
}
cout << "窗口" << window_id << "售票结束" << endl;
}
int main() {
vector<thread> windows;
// 创建 5 个售票窗口
for (int i = 1; i <= 5; ++i) {
windows.emplace_back(sell_tickets, i);
}
// 等待所有窗口售票结束
for (auto& t : windows) {
t.join();
}
cout << "所有票已售罄" << endl;
return 0;
}
这样跑下来,5 个窗口有序售票,最终票数从 100 递减到 0,不会出现数据不一致的情况。
总结
多线程访问共享资源必须同步,否则会有数据竞争风险。std::mutex 搭配 std::lock_guard 是管理锁生命周期的最佳实践。死锁可以通过固定加锁顺序或使用 std::lock 来规避。核心原则就是保护好临界区,确保同一时刻只有一个线程在执行关键逻辑。


