[操作系统]操作系统03:并发、同步与死锁
操作系统中的并发、同步与死锁
本文从工程实践角度概述操作系统中的并发、同步与死锁问题,介绍进程与线程的基本概念、临界区和竞争条件、常见同步原语、死锁产生的条件和处理策略,并给出若干设计模式和调试建议,适合作为并发程序设计和系统开发的技术参考。

图1:两个线程交替访问共享资源的时间轴示意(示意)。

图2:常用同步原语相对使用频度的示意图(示意)。

图3:两个进程持有并等待资源形成循环等待的等待图示意(示意)。
|
概念 |
描述 |
典型操作系统机制 |
说明 |
|
进程 |
具有独立地址空间和资源的执行实体。 |
进程表、调度器、资源管理。 |
进程之间相互隔离,通过IPC进行通信。 |
|
线程 |
同一进程内共享内存和文件描述符的执行单元。 |
用户级或内核级线程。 |
切换开销小、易于共享,但更容易产生竞争条件。 |
|
临界区 |
必须在任意时刻仅由一个线程执行的代码区域。 |
互斥锁、原子操作等。 |
保护不当会导致数据竞争和状态不一致。 |
|
竞争条件 |
执行结果取决于并发操作的时序或交错方式。 |
通过锁协议或事务内存进行控制。 |
往往表现为偶发问题,难以复现和调试。 |
表1:操作系统并发相关的核心概念。
|
同步原语 |
作用 |
典型用法 |
常见问题 |
|
互斥锁 |
为共享资源提供排他访问。 |
保护临界区和共享数据结构。 |
死锁、优先级反转、忘记释放锁。 |
|
信号量 |
对有限资源进行计数控制或信号通知。 |
控制N个相同资源访问;生产者-消费者模型。 |
初值设置不当或post/wait不匹配会引入隐蔽错误。 |
|
条件变量 |
在条件满足之前阻塞线程。 |
等待队列非空等事件。 |
必须与互斥锁配合使用,并在唤醒后重新检查条件。 |
|
读写锁 |
允许多个读者或一个写者。 |
读多写少的共享资源。 |
若读者过多可能导致写者饥饿;加锁顺序复杂。 |
表2:常见同步原语的作用、典型用法及常见问题。
|
条件 |
定义 |
示例 |
缓解策略 |
|
互斥 |
至少有一个资源以不可共享方式被占用。 |
对设备或数据结构施加排他锁。 |
尽可能采用可共享资源或无锁结构。 |
|
占有并等待 |
进程在保持已占有资源的同时请求新的资源。 |
线程持有锁A并等待锁B。 |
要求一次性申请所有资源或在申请新资源前释放已有资源。 |
|
不可抢占 |
资源不能被强制从进程手中夺走。 |
非抢占式锁。 |
允许资源抢占或通过事务回滚撤销操作。 |
|
循环等待 |
若干进程形成循环,每个都等待下一个进程所持有的资源。 |
P1等待R2,P2等待R3,...,Pn等待R1。 |
对资源获取施加全局顺序,避免形成环路。 |
表3:经典死锁条件及对应缓解策略。
1. 操作系统中的并发
现代操作系统通过进程和线程支持多项活动的并发执行。并发可以提高CPU利用率,实现计算与I/O的重叠,并提升交互式应用的响应性。
然而,并发也引入了额外复杂性。对内存、文件和设备等共享资源的访问必须进行协调,否则会出现竞争条件和状态不一致。理解操作系统提供的并发机制是设计正确系统的重要基础。
2. 进程、线程与共享状态
进程是具有独立地址空间、打开文件和其他资源的执行实体;线程则是在进程内部共享这些资源的轻量级执行单元。许多并发程序以线程为主要抽象,以减少上下文切换开销并简化数据共享。
线程之间的共享状态既带来便利,也带来风险。一方面,它使得通信和协作十分高效;另一方面,未经控制的共享会导致竞争条件、更新丢失和依赖时序的隐蔽错误。操作系统和语言运行时通过锁和原子操作等机制来协调访问。
3. 临界区与竞争条件
临界区是访问共享数据结构并需要保证互斥的代码区域。经典要求是任意时刻仅有一个线程能够执行临界区。若未正确实现该属性,系统行为将依赖于调度和时序,并可能出现不可预测的错误。
竞争条件指多个线程在缺乏适当同步的情况下访问共享状态,导致执行结果取决于操作交错方式。部分竞争条件可能是无害的,但许多会导致间歇性故障,难以复现。明确划定临界区并使用合适的同步原语进行保护,是并发程序设计的核心任务之一。
4. 同步原语
操作系统和并发库提供了多种同步原语。互斥锁用于互斥访问,信号量则提供计数型同步,用于控制有限资源或进行信号通知;条件变量用于在逻辑条件满足前阻塞线程;读写锁允许多个读者或一个写者。
选择合适的同步原语取决于访问模式和性能需求。例如,对低竞争资源,一个简单互斥锁即可;对读多写少的共享数据,读写锁可以提高吞吐。在选择原语时需要考虑公平性、可能的饥饿现象以及上下文切换或忙等的代价。
5. 死锁的定义和条件
死锁指一组进程或线程永久阻塞,每个都在等待被该集合中其他成员持有的资源。经典模型给出了死锁发生的四个必要条件:互斥、占有并等待、不可抢占和循环等待。
在实际系统中,死锁通常表现为应用似乎“卡住”,线程阻塞在同步原语上。诊断死锁需要了解哪些资源被持有、哪些资源被请求,常借助锁依赖分析工具或等待图来观察关系。
6. 死锁处理策略
操作系统和应用可采用多种策略处理死锁。死锁预防通过设计资源获取协议破坏至少一个必要条件,例如对锁获取施加全局顺序,或要求一次性申请所有资源。
死锁避免依赖运行时分析,仅在系统保持安全状态时才授予资源请求;死锁检测则周期性检查等待图中是否存在环,并可通过终止或回滚进程等方式进行恢复。在许多工程实践中,往往通过简单的预防规则配合良好的设计和测试来降低死锁风险。
7. 实用设计模式
若干设计模式有助于减少并发错误:限制共享可变状态的范围,在可能情况下使用不可变数据;在合适场景下优先使用消息传递或队列而非直接共享。将共享数据及其同步封装在明确的模块内部,也有助于避免错误使用。
锁顺序是一项重要实践:定义并记录全局锁获取顺序有助于避免循环等待。此外,保持临界区尽量短,并避免在持锁期间执行耗时操作,可以降低争用并提升响应性。
8. 并发系统的调试与测试
并发错误的调试十分困难,因为许多问题依赖于罕见的时序条件。压力测试、随机化调度以及系统化探索交错方式有助于暴露问题;记录锁获取和释放事件,或使用专门工具可视化等待关系,往往是分析死锁的必要手段。
通过在不同负载和配置下对并发组件进行单元测试,结合针对数据竞争的静态分析,可以提升系统可靠性。但并发错误仍可能在生产环境中出现,因此需要配套的监控和故障分析机制。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)