Skip to content

Commit a57d97f

Browse files
authored
Create cpp-sync-deadlock-risks.md
1 parent 27394d7 commit a57d97f

1 file changed

Lines changed: 196 additions & 0 deletions

File tree

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
Title: 理解 C++ 同步原语的死锁风险
2+
Date: 2025-06-22 10:00
3+
Modified: 2025-06-22 10:00
4+
Tags: C++, 并发编程, 死锁, 多线程, mutex, condition_variable, semaphore
5+
Slug: cpp-sync-deadlock-risks
6+
7+
8+
并发编程中最容易出错的地方之一,是对同步原语的误用,特别是死锁问题。使用 `std::mutex``std::condition_variable``std::semaphore` 等机制时,一些表面上看似合理的代码,可能在某些边界条件下导致程序卡死,且难以调试。
9+
10+
本文将列举一些典型易错模式,并提供对应的改进建议。
11+
12+
## std::mutex 的常见误用模式
13+
14+
### 1.1 多个 mutex 锁顺序不一致导致死锁
15+
```cpp
16+
// Thread A
17+
std::lock_guard<std::mutex> lock1(mutex1);
18+
std::lock_guard<std::mutex> lock2(mutex2);
19+
// Thread B
20+
std::lock_guard<std::mutex> lock2(mutex2);
21+
std::lock_guard<std::mutex> lock1(mutex1); // ❌ 死锁风险
22+
```
23+
**问题分析**:两个线程以不同的顺序加锁,可能互相等待对方释放资源,造成死锁。
24+
**建议**:
25+
* 统一加锁顺序(如按锁的内存地址排序)
26+
* 使用 `std::scoped_lock` 一次性加多个锁(C++17 起):
27+
```cpp
28+
std::scoped_lock lock(mutex1, mutex2);
29+
```
30+
31+
### 1.2 在持锁状态下调用外部函数
32+
```cpp
33+
std::lock_guard<std::mutex> lock(mutex);
34+
some_external_function(); // ❌ 如果该函数内部也尝试加锁,可能死锁
35+
```
36+
**建议**:避免在持锁状态下调用外部函数。必要时应将调用逻辑移至锁作用域之外。
37+
38+
### 1.3 锁中递归调用自身或间接调用自己
39+
```cpp
40+
std::mutex m;
41+
void foo() {
42+
std::lock_guard<std::mutex> lock(m);
43+
bar();
44+
}
45+
void bar() {
46+
foo(); // ❌ 间接递归调用,重复加锁,死锁
47+
}
48+
```
49+
**建议**
50+
* 避免递归持有 `std::mutex`
51+
* 若必须递归使用,应改用 `std::recursive_mutex`,但应慎用。
52+
53+
### 1.4 使用裸指针或悬空 mutex 对象
54+
```cpp
55+
std::mutex* m = get_mutex();
56+
std::lock_guard<std::mutex> lock(*m); // ❌ 如果 m 已析构,行为未定义
57+
58+
```
59+
**问题分析**:多个线程共享裸 mutex 指针,生命周期不可控,易造成悬空引用或崩溃。
60+
**建议**:
61+
* 使用智能指针管理 mutex 生命周期。
62+
* 或将 mutex 作为静态变量或类成员持有。
63+
64+
### 1.5 手动加锁,异常或提前 return 导致未解锁
65+
```cpp
66+
m.lock();
67+
if (error)
68+
return; // ❌ 忘记解锁,死锁风险
69+
m.unlock();
70+
```
71+
**建议**:始终使用 RAII 风格的 `std::lock_guard``std::unique_lock`,避免忘记解锁:
72+
```cpp
73+
std::lock_guard<std::mutex> lock(m);
74+
if (error)
75+
return; // ✅ 安全
76+
```
77+
78+
## std::recursive_mutex 的误用风险
79+
虽然 `std::recursive_mutex` 允许同一线程多次加锁,但它**不是死锁的万灵药**,常常掩盖设计缺陷或状态混乱。
80+
### 示例问题:资源未及时释放
81+
```cpp
82+
std::recursive_mutex m;
83+
void foo() {
84+
std::lock_guard<std::recursive_mutex> lock(m);
85+
// 很长的逻辑,期间可能递归调用自身
86+
}
87+
```
88+
**问题分析**:虽然不会死锁,但锁持有时间可能过长,降低系统并发性。
89+
**建议**
90+
* 优先重构逻辑,避免递归加锁。
91+
* 仅在确实需要递归的少数场景使用。
92+
93+
## std::condition_variable 的常见陷阱
94+
95+
### 3.1 忘记使用谓词检查条件
96+
```cpp
97+
cv.wait(lock); // ❌ 如果通知早于 wait,线程会永久阻塞
98+
```
99+
**建议**:始终使用谓词版本:
100+
```cpp
101+
cv.wait(lock, [] { return condition; });
102+
```
103+
**理由**:防止虚假唤醒和时序问题。
104+
105+
### 3.2 误用 `notify_one()` 与 `notify_all()`
106+
107+
```cpp
108+
cv.notify_one(); // ❌ 多线程等待时,可能遗漏唤醒
109+
```
110+
**建议**
111+
* 多线程等待时,优先使用 `notify_all()`
112+
* 确保 notify 发生时 mutex 已持有,避免竞态。
113+
114+
### 3.3 wait 前未持有锁
115+
116+
```cpp
117+
cv.wait(lock); // ❌ lock 未加锁,行为未定义
118+
```
119+
**建议**:必须先通过 `std::unique_lock<std::mutex>` 加锁,确保 `wait()` 调用合法:
120+
```cpp
121+
std::unique_lock<std::mutex> lock(m);
122+
cv.wait(lock, [] { return condition; });
123+
```
124+
125+
## std::semaphore 的使用误区(C++20 起)
126+
127+
### 4.1 忘记配对 `release()`
128+
```cpp
129+
std::binary_semaphore sem(0);
130+
sem.acquire(); // ❌ 若未调用 release,线程将永久阻塞
131+
```
132+
133+
**建议**
134+
* 保证每次 `acquire()` 有对应的 `release()`
135+
* 使用带超时版本避免无限阻塞:
136+
```cpp
137+
if (!sem.try_acquire_for(std::chrono::seconds(2))) {
138+
// 超时处理逻辑
139+
}
140+
```
141+
142+
### 4.2 信号丢失:acquire 在 release 之后调用
143+
144+
```cpp
145+
std::binary_semaphore sem(0);
146+
147+
void notify() {
148+
sem.release(); // ✅ 提前释放
149+
}
150+
151+
void wait() {
152+
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // ❌ 晚于 release 执行
153+
sem.acquire(); // ❌ 永久阻塞风险
154+
}
155+
```
156+
157+
**问题分析**:
158+
159+
* `std::binary_semaphore` 类似一个布尔标志,仅记录是否被释放过一次。
160+
* 如果 `release()` 先于 `acquire()` 调用,且调用之间没有明确同步关系,信号可能“被丢失”。
161+
* 由于 `binary_semaphore` 不累加信号(只能表示 0 或 1),先发生的 `release()` 在没有等待者时不会保存“通知”。
162+
* 此问题在“先通知、后等待”场景中尤为常见,与 `condition_variable` 的使用误区类似。
163+
164+
**建议**:
165+
166+
* 保证 `acquire()` 的调用时机在 `release()` 之前或两者明确同步。
167+
* 如需支持先通知后等待,改用 `std::counting_semaphore`,它支持信号积累。
168+
* 或使用超时接口避免无限阻塞:
169+
170+
```cpp
171+
if (!sem.try_acquire_for(std::chrono::seconds(2))) {
172+
// 超时处理逻辑
173+
}
174+
```
175+
176+
### 4.3 多线程共享时释放不足
177+
```cpp
178+
std::binary_semaphore sem(0);
179+
void worker() {
180+
sem.acquire();
181+
// 做一些工作
182+
}
183+
void notify() {
184+
sem.release(); // ❌ 仅释放一次,其余线程会永久等待
185+
}
186+
```
187+
**建议**:
188+
* 对 N 个等待线程,应调用 N 次 `release()`。
189+
* 或使用 `counting_semaphore` 表示资源总量。
190+
191+
## 5. 总结建议
192+
* 所有锁的使用都应**结构化管理**,优先使用 RAII(如 `std::lock_guard`)。
193+
* 加锁逻辑应**保持一致性**,避免递归加锁、交叉持锁、或生命周期不一致。
194+
* 条件变量使用时应始终搭配**谓词检查**,避免时序或虚假唤醒问题。
195+
* 对 `semaphore` 等底层原语,应设置**超时保护**,防止意外永久阻塞。
196+
* 尽量避免裸指针、手动 lock/unlock 以及难以维护的状态共享逻辑。

0 commit comments

Comments
 (0)