在第 17 节中,我们学习了多线程编程的基础概念和 std::thread 的使用。本节将深入讲解线程同步 的核心工具------互斥锁 (Mutex),解决多线程编程中最常见的问题:数据竞争。
1. 数据竞争:多线程的头号敌人
1.1 什么是数据竞争?
当两个或多个线程同时读写 同一块内存,且至少有一个线程在写入时,就会发生数据竞争 (Data Race)。数据竞争的结果是未定义行为,程序可能崩溃、产生错误结果,或者看似正常但偶尔出问题。
cpp
#include <iostream>
#include <thread>
using namespace std;
int counter = 0;
void increment() {
for (int i = 0; i < 100000; i++) {
counter++; // 这不是原子操作!
}
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
// 期望 200000,实际结果不确定
cout << "counter = " << counter << endl;
return 0;
}
运行多次,结果可能为 134567、178234 等各种随机值,永远不是 200000。
1.2 为什么 counter++ 不安全?
counter++ 看似一条语句,实际包含三个步骤:
- 读取:从内存读取 counter 的值到寄存器
- 修改:在寄存器中将值加 1
- 写回:将新值写回内存
如果两个线程同时执行,可能的交错情况:
线程A:读取 counter = 100
线程B:读取 counter = 100
线程A:写入 counter = 101
线程B:写入 counter = 101 ← 丢失了一次自增!
2. std::mutex:最基本的互斥锁
2.1 什么是互斥锁?
互斥锁(Mutex,Mutual Exclusion 的缩写)就像一把门锁。当一个线程进入临界区时,它把锁锁上,其他线程必须等待锁被释放才能进入。
线程A:加锁 → 访问共享数据 → 解锁
线程B:等待... → 加锁 → 访问共享数据 → 解锁
2.2 修复数据竞争
cpp
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int counter = 0;
mutex mtx;
void increment() {
for (int i = 0; i < 100000; i++) {
mtx.lock();
counter++;
mtx.unlock();
}
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
cout << "counter = " << counter << endl; // 总是 200000
return 0;
}
运行结果:
counter = 200000
2.3 lock() 和 unlock() 的注意事项
手动调用 lock() 和 unlock() 有一个致命问题:如果在 lock() 之后、unlock() 之前发生异常,锁永远不会被释放,导致死锁。
cpp
void riskyFunction() {
mtx.lock();
// 如果这里抛出异常...
doSomething(); // ← 异常!
mtx.unlock(); // ← 永远不会执行!
}
3. std::lock_guard:RAII 自动锁
3.1 什么是 lock_guard?
std::lock_guard 是一个 RAII 风格的锁管理器。它在构造时加锁,在析构时自动解锁,即使发生异常也能保证锁被释放。
cpp
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int counter = 0;
mutex mtx;
void increment() {
for (int i = 0; i < 100000; i++) {
lock_guard<mutex> lock(mtx); // 构造时加锁
counter++;
// 离开作用域时自动解锁
}
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
cout << "counter = " << counter << endl; // 200000
return 0;
}
3.2 作用域决定锁的范围
lock_guard 的生命周期就是锁的范围。可以用花括号控制:
cpp
void example() {
cout << "这里没有锁" << endl;
{
lock_guard<mutex> lock(mtx);
cout << "这里是临界区,锁生效" << endl;
// 在花括号结束时自动解锁
}
cout << "锁已经释放" << endl;
}
4. std::unique_lock:更灵活的锁
4.1 与 lock_guard 的区别
std::unique_lock 比 lock_guard 更灵活,支持:
- 延迟加锁:构造时不立即加锁
- 手动解锁和重新加锁 :
unlock()和lock() - 转移所有权:配合条件变量使用
cpp
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
mutex mtx;
void flexibleLocking() {
unique_lock<mutex> lock(mtx, defer_lock); // 不立即加锁
cout << "做一些不需要锁的操作..." << endl;
lock.lock(); // 现在加锁
cout << "临界区操作" << endl;
lock.unlock(); // 手动解锁
cout << "做一些不需要锁的操作..." << endl;
lock.lock(); // 可以再次加锁
cout << "再次进入临界区" << endl;
}
int main() {
thread t1(flexibleLocking);
t1.join();
return 0;
}
4.2 延迟加锁(defer_lock)
cpp
// 方式一:构造时加锁(默认)
unique_lock<mutex> lock1(mtx);
// 方式二:延迟加锁
unique_lock<mutex> lock2(mtx, defer_lock);
lock2.lock(); // 需要时再加锁
// 方式三:尝试加锁(不阻塞)
unique_lock<mutex> lock3(mtx, try_to_lock);
if (lock3.owns_lock()) {
// 成功获取锁
}
4.3 lock_guard vs unique_lock
| 特性 | lock_guard | unique_lock |
|---|---|---|
| 构造时加锁 | 必须 | 可选 |
| 手动 unlock/lock | 不支持 | 支持 |
| 转移所有权 | 不支持 | 支持 |
| 性能 | 略快 | 略慢(更多检查) |
| 使用场景 | 简单临界区 | 复杂同步需求 |
建议 :简单场景用 lock_guard,需要灵活性时用 unique_lock。
5. std::scoped_lock(C++17)
5.1 同时锁多个互斥量
std::scoped_lock 可以同时锁住多个互斥量,避免死锁。
cpp
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
mutex mtx1, mtx2;
void threadA() {
scoped_lock lock(mtx1, mtx2); // 同时锁住两个锁
cout << "线程A:同时持有两把锁" << endl;
}
void threadB() {
scoped_lock lock(mtx1, mtx2); // 相同顺序,不会死锁
cout << "线程B:同时持有两把锁" << endl;
}
int main() {
thread t1(threadA);
thread t2(threadB);
t1.join();
t2.join();
return 0;
}
5.2 为什么 scoped_lock 能避免死锁?
如果手动按不同顺序加锁:
cpp
// 线程A
mtx1.lock();
mtx2.lock(); // 死锁风险!
// 线程B
mtx2.lock();
mtx1.lock(); // 死锁!
scoped_lock 内部使用死锁避免算法,无论你以什么顺序传入,都能安全加锁。
6. 读写锁:std::shared_mutex(C++17)
6.1 读多写少的场景
在很多场景中,读操作远多于写操作。如果用普通互斥锁,读操作之间也会互斥,浪费性能。
std::shared_mutex 支持:
- 共享锁(读锁):多个线程可以同时持有
- 独占锁(写锁):只有一个线程能持有
cpp
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>
using namespace std;
shared_mutex rwMutex;
int sharedData = 0;
void reader(int id) {
shared_lock<shared_mutex> lock(rwMutex); // 共享锁
cout << "读者 " << id << " 读取数据:" << sharedData << endl;
}
void writer(int id, int value) {
unique_lock<shared_mutex> lock(rwMutex); // 独占锁
sharedData = value;
cout << "写者 " << id << " 写入数据:" << value << endl;
}
int main() {
vector<thread> threads;
// 启动多个读者和写者
for (int i = 0; i < 5; i++) {
threads.emplace_back(reader, i);
}
threads.emplace_back(writer, 1, 100);
threads.emplace_back(writer, 2, 200);
for (int i = 5; i < 10; i++) {
threads.emplace_back(reader, i);
}
for (auto& t : threads) t.join();
return 0;
}
6.2 shared_lock 和 unique_lock 的配合
cpp
shared_mutex rwMutex;
// 读操作:使用 shared_lock
void readData() {
shared_lock<shared_mutex> lock(rwMutex);
// 多个线程可以同时执行这里
readFromDatabase();
}
// 写操作:使用 unique_lock
void writeData() {
unique_lock<shared_mutex> lock(rwMutex);
// 只有一个线程能执行这里
writeToDatabase();
}
7. 原子操作:std::atomic
7.1 什么是原子操作?
对于简单的计数器等场景,使用互斥锁有点重。std::atomic 提供了无锁的线程安全操作。
cpp
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
atomic<int> counter(0);
void increment() {
for (int i = 0; i < 100000; i++) {
counter++; // 原子操作,线程安全
}
}
int main() {
thread t1(increment);
thread t2(increment);
t1.join();
t2.join();
cout << "counter = " << counter << endl; // 总是 200000
return 0;
}
7.2 atomic 的常用操作
cpp
atomic<int> a(0);
a.store(10); // 写入
int val = a.load(); // 读取
a.exchange(20); // 交换,返回旧值
// 复合操作
a.fetch_add(5); // 原子加
a.fetch_sub(3); // 原子减
a.fetch_and(0xFF); // 原子与
a.fetch_or(0x01); // 原子或
// 前缀/后缀自增自减
a++;
++a;
a--;
--a;
// 比较并交换(CAS)
int expected = 10;
a.compare_exchange_strong(expected, 20);
// 如果 a == 10,则 a = 20,返回 true
// 如果 a != 10,则 expected = a 的值,返回 false
7.3 atomic vs mutex
| 特性 | atomic | mutex |
|---|---|---|
| 适用范围 | 单个变量 | 代码块 |
| 性能 | 非常快(无锁) | 较慢(有锁) |
| 复杂操作 | 不支持 | 支持 |
| 死锁风险 | 无 | 有 |
建议 :简单计数器、标志位用 atomic,复杂数据结构用 mutex。
8. 常见陷阱与最佳实践
8.1 不要返回引用给共享数据
cpp
// 错误:返回引用后,锁已释放,调用者访问的是未保护的数据
int& getCounter() {
lock_guard<mutex> lock(mtx);
return counter; // 危险!
}
// 正确:返回副本
int getCounter() {
lock_guard<mutex> lock(mtx);
return counter; // 返回拷贝
}
8.2 锁的粒度要合适
cpp
// 粒度太粗:整个函数都被锁住
void bad() {
lock_guard<mutex> lock(mtx);
doSlowComputation(); // 这部分不需要锁
updateSharedData(); // 只有这里需要锁
}
// 粒度合适:只锁必要的部分
void good() {
doSlowComputation(); // 无锁
lock_guard<mutex> lock(mtx);
updateSharedData(); // 有锁
}
8.3 避免嵌套锁
cpp
// 危险:可能导致死锁
void dangerous() {
lock_guard<mutex> lock1(mtxA);
lock_guard<mutex> lock2(mtxB); // 如果其他线程反序加锁 → 死锁
}
// 安全:使用 scoped_lock
void safe() {
scoped_lock lock(mtxA, mtxB); // 自动避免死锁
}
9. 总结
本节我们学习了线程同步的核心工具:
- 数据竞争:多线程同时读写共享数据导致的未定义行为
- std::mutex :基本互斥锁,
lock()/unlock() - std::lock_guard:RAII 自动锁,简单场景首选
- std::unique_lock:灵活锁,支持延迟加锁和条件变量
- std::scoped_lock(C++17):同时锁多个锁,避免死锁
- std::shared_mutex(C++17):读写锁,适合读多写少场景
- std::atomic:无锁原子操作,适合简单变量
下一节我们将学习条件变量,它是线程间通信的核心工具,用于实现生产者-消费者模型等经典并发模式。