%% # 纲要 > 主干纲要、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》 | 《操作系统导论》