%%
# 纲要
> 主干纲要、Hint/线索/路标
# Q&A
#### 已明确
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
%%
# 常见并发问题
- **违反原子性**(atomicity violation) => 需要**确保操作的原子性**,可使用 "**==互斥锁==**"
- **错误顺序**(order violation) => 需要**保证多个线程间明确的==先后执行顺序==**,可使用 "**==条件变量==**"(或 **==信号量==**)
- **死锁**(deadlock)
# 临界区
> [!info] 临界区——指访问 "**共享变量**"(共享资源) 的**代码片段**。
>
> **==临界区一定不能由 "多个线程同时执行"==** ,否则可能导致 "**竞态条件**" 问题,应当**加上互斥锁或其他同步机制来确保共享数据的互斥访问**。
>
<br><br>
# 条件竞争
> Race Condition.
「**==条件竞争==**」: **多个线程同时访问==共享资源==时,==由于可能发生 "线程上下文切换"(时钟中断触发)==,在没有锁或其他同步机制的情况下,会使得各线程操作顺序不确定,导致预期结果具有不确定性**。
> [!NOTE] "线程安全"——指多线程环境下,多个线程并发访问共享资源,不会出现条件竞争、数据不一致问题。
> [!example] 示例场景
>
> **多个线程对同一个共享变量进行"读-改-写"操作时,可能由于发生线程切换,导致两进程==读取到相同的原变量值==到各自寄存器后进行相同修改再写回内存**,使得最终结果表现为只执行了一次操作。
>
> ```cpp
> int count = 0;
> int ops = 1000000;
>
> void threadFunc() {
> for (int i = 0; i < ops; ++i) {
> ++count; // 临界区, 存在条件竞争
> }
> }
>
> int main() {
> thread t1(threadFunc), t2(threadFunc);
> t1.join();
> t2.join();
> cout << count << endl; // count可能的结果值: [ops, 2ops]
> }
> ```
>
>
### 避免条件竞争的方法
关键:确保对共享数据的 "**互斥访问**"——如果**一个线程在临界区中执行**,**其他线程要被阻止进入临界区**。
例如,**加上==互斥锁==或其他同步机制来==确保对共享数据的互斥访问==**。
%%
![[_attachment/02-开发笔记/05-操作系统/并发相关/并发与并行.assets/IMG-并发与并行-97417A49B2EB7624CF0038EE4236D2F8.png|476]]
%%
<br><br><br>
# 死锁
> ❓<font color="#c00000"> 什么是死锁?</font>
死锁:多个进程或线程由于**相互等待**而永远无法继续执行的一种状态。
## 形成死锁的四个必要条件
- **==互斥==**(Mutual Exclusion):进程 or 线程间**对于所需资源的访问是互斥的**,即**一个资源只能==同时==被一个进程 or 线程==独占==**
- **==持有并等待==**(Hold and Wait):进程 or 线程**已持有至少一个资源,同时又在等待其他资源,==等待过程中不释放已占用资源==**<br>(例如已持有一个锁,又在等待另一个需获得的锁)
- **==非抢占==**(No Preemption):进程 or 线程**已获取的资源==不可被强制剥夺抢占==**,**只能由其自己释放**(OS 也不能收回其资源)
- **==循环等待==**(Circular Wait):进程 or 线程间形成一个**等待环路**,环路上**每一方都持有一个资源**,**同时==都等待着下一方占有的资源==**
![[_attachment/02-开发笔记/05-操作系统/并发相关/常见并发问题.assets/IMG-常见并发问题-590C962FE18F072FF1DB87F76D12E6FB.png|394]]
## 避免死锁的方法
破除形成死锁的必要条件之一即可[^2](P284):
##### 破坏 "循环等待"(最常采用)
**对资源有序分配**,使得**各个进程 or 线程获取资源的顺序均相同即可**,例如都先申请锁 A 再申请锁 B
示例:![[_attachment/02-开发笔记/05-操作系统/并发相关/常见并发问题.assets/IMG-常见并发问题-9FD066EB2B8897AB6F5E8C7B2961524B.png|653]]
##### 破坏 "**持有并等待**"
将获取多个锁的过程 "**==原子化==**"——每个进程 or 线程**要么原子地获取所有所需锁,要么一个锁也不获取**。
![[_attachment/02-开发笔记/05-操作系统/并发相关/常见并发问题.assets/IMG-常见并发问题-2F7B197231B99BB4306FD71587C12F96.png|684]]
在获取多个锁时,如果无法一次获取到所有锁,则 **==释放已获得的锁==**:
![[_attachment/02-开发笔记/05-操作系统/并发相关/常见并发问题.assets/IMG-常见并发问题-0D9F68292CDDB84F7E670ADD8F11DBE9.png|665]]
##### 破坏 "互斥"
略,需要特定 "**硬件指令**" 的支持,从而实现**在访问临界区时不使用 "锁" 而进行原子操作**,不用锁,也就**不会形成死锁等待的场景**。
<br><br>
%%
## 死锁示例——哲学家进餐问题
%%
## 排查死锁的工具 & 方法
Linux 下可使用 **pstack & gdb** 定位死锁问题[^1]。
- `pstack <pid>` 命令**显示进程中每个线程的栈跟踪信息**,同多次执行 pstack 命令**查看线程的函数调用过程**,可以观察是哪几个线程一直无变化,由此**推测其可能是一直在等待锁**
- `gdb` 调试运行:
- **通过 `info thread` 查看所有线程信息**, **通过 `tread <n>` 切换到不同线程**,**通过 `bt` & `frame <n>` 查看具体栈帧信息**,**确定各个线程分别==等待的是哪个互斥量==**;
- **通过 `p mutex` 打印具体==互斥量== `mutex`** 的信息,查看其被哪个线程持有。
参见 [[05-工具/GNU 工具/gdb 调试器#示例:死锁检测|gdb 调试器-示例: 死锁检测]]
<br><br>
# 参考资料
# Footnotes
[^1]: [5.4 怎么避免死锁? | 小林coding](https://xiaolincoding.com/os/4_process/deadlock.html#%E6%A8%A1%E6%8B%9F%E6%AD%BB%E9%94%81%E9%97%AE%E9%A2%98%E7%9A%84%E4%BA%A7%E7%94%9F)
[^2]: 《OSTEP》 | 《操作系统导论》