操作系统中的并发、同步与死锁

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

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. 并发系统的调试与测试

并发错误的调试十分困难,因为许多问题依赖于罕见的时序条件。压力测试、随机化调度以及系统化探索交错方式有助于暴露问题;记录锁获取和释放事件,或使用专门工具可视化等待关系,往往是分析死锁的必要手段。

通过在不同负载和配置下对并发组件进行单元测试,结合针对数据竞争的静态分析,可以提升系统可靠性。但并发错误仍可能在生产环境中出现,因此需要配套的监控和故障分析机制。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐