在多线程编程中,当多个线程访问共享资源时,可能会出现"数据竞争"(data race),导致程序行为不可预测。锁(Lock) 是解决这一问题的核心同步机制,它通过保证"临界区"(critical section,即访问共享资源的代码段)的互斥访问,确保多线程安全。
对于死锁,我脑子中的例子是:两个人A和B,手里都有一个汉堡,现在都需要拿桌子上的番茄酱和芝士,挤压到汉堡上,A拿了番茄,B拿了芝士,此时A等芝士,B等番茄,就发生了死锁。解决死锁的思路可以:同时锁,或者获取锁的顺序一致,或者降低锁的时间等等吧。关于死锁的四个必要条件,下面有详细介绍,记一下。
一、C++中的锁类型
C++标准库(C++11及后续版本)提供了多种锁类型,适用于不同场景,均定义在 <mutex>
头文件中。
1. 基础互斥锁:std::mutex
std::mutex
是最基本的互斥锁,提供独占性访问控制:同一时间只能有一个线程获得锁,其他线程尝试获取锁时会阻塞,直到锁被释放。
核心方法:
lock()
:获取锁(若已被其他线程持有,则当前线程阻塞)。unlock()
:释放锁(必须由持有锁的线程调用,否则行为未定义)。try_lock()
:尝试获取锁(若成功返回true
;若已被持有,立即返回false
,不阻塞)。
使用注意:
必须手动调用 lock()
和 unlock()
,且需保证成对出现(否则可能导致死锁)。实际开发中通常配合RAII工具(如std::lock_guard
)使用,避免遗漏解锁。
#include <mutex>
#include <thread>
std::mutex mtx; // 全局互斥锁
int shared_data = 0;
void increment() {
mtx.lock(); // 获取锁
shared_data++; // 临界区:修改共享资源
mtx.unlock(); // 释放锁(必须调用,否则其他线程永远阻塞)
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
// shared_data 最终一定是 2(无数据竞争)
return 0;
}
2. 递归互斥锁:std::recursive_mutex
std::recursive_mutex
允许同一线程多次获取锁(不会阻塞自身),但需要对应次数的解锁(unlock()
)才能释放。
适用场景:
当一个函数递归调用自身,且每次调用都需要获取锁时(若用std::mutex
会导致线程自己阻塞自己,造成死锁)。
注意:
递归锁的开销比普通std::mutex
略大,非必要不使用(过度使用可能掩盖设计问题)。
#include <mutex>
std::recursive_mutex rmtx;
int count = 0;
void recursive_func(int depth) {
if (depth <= 0) return;
rmtx.lock();
count++;
recursive_func(depth - 1); // 同一线程再次获取锁,不会死锁
rmtx.unlock();
}
int main() {
recursive_func(5); // count 最终为 5
return 0;
}
3. 定时互斥锁:std::timed_mutex
与 std::recursive_timed_mutex
这两种锁支持超时获取锁,避免线程无限期阻塞。
std::timed_mutex
:非递归版本,支持超时。std::recursive_timed_mutex
:递归版本,支持超时。
核心方法(新增):
try_lock_for(std::chrono::duration)
:在指定时间段内尝试获取锁(超时返回false
)。try_lock_until(std::chrono::time_point)
:在指定时间点前尝试获取锁(超时返回false
)。
适用场景:
需要限制等待锁的时间(如避免程序因死锁完全挂起)。
#include <mutex>
#include <chrono>
#include <thread>
std::timed_mutex tmtx;
void try_lock_with_timeout() {
// 尝试在100毫秒内获取锁
if (tmtx.try_lock_for(std::chrono::milliseconds(100))) {
// 成功获取锁,执行临界区
tmtx.unlock();
} else {
// 超时,执行备选逻辑
}
}
4. 共享互斥锁:std::shared_mutex
(C++17引入)
std::shared_mutex
支持两种访问模式:
- 独占模式(写模式):同一时间只能有一个线程获取(类似
std::mutex
),用于修改共享资源。 - 共享模式(读模式):多个线程可同时获取,用于读取共享资源。
核心方法:
- 独占模式:
lock()
,unlock()
,try_lock()
(同std::mutex
)。 - 共享模式:
lock_shared()
,unlock_shared()
,try_lock_shared()
。
适用场景:
"读多写少"的场景(如缓存、配置数据),多个读线程可并发访问,提高效率。
#include <shared_mutex>
#include <thread>
#include <vector>
std::shared_mutex smtx;
int shared_value = 0;
// 读操作(共享模式)
int read_value() {
std::shared_lock<std::shared_mutex> lock(smtx); // 获取共享锁
return shared_value;
}
// 写操作(独占模式)
void write_value(int new_val) {
std::unique_lock<std::shared_mutex> lock(smtx); // 获取独占锁
shared_value = new_val;
}
int main() {
// 多个读线程可同时执行
std::vector<std::thread> readers;
for (int i = 0; i < 5; ++i) {
readers.emplace_back(read_value);
}
// 写线程执行时,所有读线程需等待
std::thread writer(write_value, 10);
for (auto& t : readers) t.join();
writer.join();
return 0;
}
二、锁管理工具(RAII)
手动调用lock()
和unlock()
容易出错(如异常导致解锁遗漏)。C++提供了RAII风格的锁管理工具,通过对象生命周期自动管理锁的获取与释放。
1. std::lock_guard
(C++11)
std::lock_guard
是最简单的锁管理工具:
- 构造时自动调用
lock()
获取锁。 - 析构时自动调用
unlock()
释放锁(即使发生异常也会执行)。
特点:轻量、高效,但功能简单(不支持手动解锁、转移所有权等)。
#include <mutex>
std::mutex mtx;
int data = 0;
void safe_increment() {
std::lock_guard<std::mutex> lock(mtx); // 构造时锁定
data++; // 临界区
// 析构时自动解锁(函数结束时)
}
2. std::unique_lock
(C++11)
std::unique_lock
是更灵活的锁管理工具,支持:
- 延迟锁定(构造时不立即获取锁)。
- 手动解锁(
unlock()
)和重新锁定(lock()
)。 - 超时获取锁(配合
timed_mutex
)。 - 所有权转移(通过
std::move
)。
代价:比std::lock_guard
略重(有额外状态开销)。
#include <mutex>
#include <chrono>
std::timed_mutex tmtx;
void flexible_lock() {
// 延迟锁定(不立即获取锁)
std::unique_lock<std::timed_mutex> lock(tmtx, std::defer_lock);
// 手动尝试超时获取锁
if (lock.try_lock_for(std::chrono::milliseconds(50))) {
// 成功获取锁
lock.unlock(); // 手动解锁
// ... 其他操作
lock.lock(); // 重新锁定
}
// 析构时自动解锁(若当前持有锁)
}
3. std::shared_lock
(C++17)
std::shared_lock
用于配合std::shared_mutex
获取共享锁(读模式),同样是RAII风格:
- 构造时调用
lock_shared()
。 - 析构时调用
unlock_shared()
。
// 配合 std::shared_mutex 的读操作
int read_with_shared_lock() {
std::shared_lock<std::shared_mutex> lock(smtx); // 获取共享锁
return shared_value;
}
三、死锁(Deadlock)与避免
死锁是多线程编程中最常见的问题:两个或多个线程互相等待对方释放锁,导致所有线程永久阻塞。
死锁的四个必要条件:
- 互斥:资源(锁)只能被一个线程持有。
- 持有并等待:线程持有一个锁,同时等待另一个锁。
- 不可剥夺:线程持有的锁不能被强制剥夺。
- 循环等待:线程A等待线程B的锁,线程B等待线程A的锁。
避免死锁的常用方法:
-
按固定顺序加锁:
所有线程获取多个锁时,严格按照相同的顺序(如按锁的地址排序),避免循环等待。std::mutex mtx1, mtx2; // 错误:顺序不一致可能导致死锁 void thread1() { mtx1.lock(); mtx2.lock(); /* ... */ } void thread2() { mtx2.lock(); mtx1.lock(); /* ... */ } // 危险! // 正确:固定顺序加锁 void thread1() { mtx1.lock(); mtx2.lock(); /* ... */ } // 先1后2 void thread2() { mtx1.lock(); mtx2.lock(); /* ... */ } // 先1后2
-
使用
std::lock
同时加锁多个锁:
std::lock
是标准库提供的函数,可原子地获取多个锁(避免部分获取导致的死锁)。std::mutex mtx1, mtx2; void safe_lock_both() { // 原子地获取 mtx1 和 mtx2(无死锁风险) std::lock(mtx1, mtx2); // 用 std::adopt_lock 告诉 lock_guard 锁已获取,无需再次 lock() std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock); std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock); // 临界区 }
-
使用超时锁:
用try_lock_for
或try_lock_until
,若超时则释放已获取的锁并重试,避免无限等待。 -
减少锁的持有时间:
仅在必要的临界区持有锁,完成后立即释放,降低死锁概率。
四、锁与其他同步机制的配合
锁常与条件变量(std::condition_variable
)配合,实现线程间的通信(如"生产者-消费者"模型)。
std::condition_variable
用于线程等待某个条件成立,需配合std::unique_lock
使用:
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> q; // 共享队列(生产者放入,消费者取出)
// 生产者
void producer() {
for (int i = 0; i < 10; ++i) {
std::lock_guard<std::mutex> lock(mtx);
q.push(i);
cv.notify_one(); // 通知一个等待的消费者
}
}
// 消费者
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
// 等待队列非空(防止虚假唤醒,用循环判断条件)
cv.wait(lock, []{ return !q.empty(); });
int val = q.front();
q.pop();
lock.unlock(); // 提前解锁,减少阻塞时间
// 处理数据
if (val == 9) break; // 结束标志
}
}
五、锁 vs 原子操作(std::atomic
)
- 原子操作:适用于简单的读写操作(如计数器
++
),由硬件直接支持,开销极小(无上下文切换)。 - 锁:适用于复杂的临界区(如多步操作、数据结构修改),开销较大(可能导致线程阻塞和上下文切换)。
选择原则:能用水原子操作解决的问题,就不用锁。
六、最佳实践
- 最小化临界区:只在必要时持有锁,避免锁的范围过大。
- 优先使用RAII工具:用
lock_guard
/unique_lock
管理锁,避免手动解锁遗漏。 - 避免嵌套锁:嵌套锁容易导致死锁,若必须使用,严格保证顺序。
- 读多写少用
shared_mutex
:提高并发读的效率。 - 警惕锁的粒度:锁太粗(一个锁保护所有资源)会降低并发;锁太细(多个锁保护细分资源)会增加死锁风险,需平衡。
总结:C++的锁机制为多线程同步提供了灵活的工具,理解各种锁的特性和适用场景,结合RAII和死锁避免技巧,才能写出安全高效的并发代码。