进程同步

本文最后更新于:2025年10月16日 下午

进程同步是多道程序环境下协调进程并发执行的核心机制。引入进程后,虽能通过并发提升资源利用率和系统吞吐量,但进程对共享资源的无序争夺会导致执行结果不可再现。进程同步的本质是通过特定规则协调进程执行次序,确保共享资源有序访问与进程间高效协作,使程序执行具备可再现性。

一、进程同步的基本概念与制约关系

1. 进程同步的核心任务

对多个相关进程的执行次序进行协调,解决“并发导致的资源争夺与协作混乱”问题,具体包括:

  • 确保进程按规则共享系统资源(如打印机、共享变量);
  • 实现进程间的有效合作(如“司机-售票员”进程的协同操作);
  • 消除并发执行的不可再现性,保证每次执行结果一致。

2. 进程间的两种制约关系

进程并发执行时,因资源共享或任务协作产生两种典型制约关系,均需同步机制协调:

(1)间接相互制约关系(资源共享导致)

  • 产生原因:多个进程共享同一临界资源(如打印机、共享内存),需通过互斥方式访问,避免资源状态被无序修改。
  • 示例:进程A和进程B均需使用打印机,若A正在打印(占用打印机),B必须等待A释放后才能使用。
  • 本质:进程间无直接通信,仅因共享资源而间接相互限制。

(2)直接相互制约关系(任务协作导致)

  • 产生原因:多个进程为完成同一任务需协同工作,一个进程的执行依赖另一个进程的结果(如“生产者-消费者”进程,生产者生产产品后消费者才能消费)。
  • 示例:司机进程(P1)需在售票员进程(P2)关门后才能启动车辆,P2需在P1到站停车后才能开门,二者执行顺序直接关联。
  • 本质:进程间存在明确的依赖关系,需通过信号或消息传递同步执行节奏。

二、临界资源与临界区

1. 临界资源(Critical Resource)

  • 定义:一次仅允许一个进程访问的资源,包括硬件资源(如打印机、磁带机)和软件资源(如共享变量、数据库记录)。
  • 核心特性:排他性——若多个进程同时访问,会导致资源状态混乱或执行结果错误。
  • 管理原则:进程对临界资源的访问必须互斥,即一个进程访问时,其他进程需等待。

2. 临界区(Critical Section)

  • 定义:每个进程中直接访问临界资源的那段代码,是进程同步的核心控制对象。
  • 进程访问临界区的标准结构:为确保互斥,进程访问临界资源需遵循“进入区→临界区→退出区→剩余区”的流程:
    1
    2
    3
    4
    5
    6
    while(TRUE){
    进入区(Entry Section):检查临界资源是否可用,若不可用则阻塞,避免“忙等”;
    临界区(Critical Section):访问临界资源的核心代码,执行时独占资源;
    退出区(Exit Section):释放临界资源,唤醒等待的进程;
    剩余区(Remainder Section):与临界资源无关的其他代码;
    }
  • 关键问题:若多个进程并发执行时,临界区代码未加控制,会导致共享资源操作异常(如共享变量计算错误)。

示例:临界区代码并发执行的问题

以“生产者-消费者”问题中共享变量counter(记录缓冲池产品数量)为例:

  • 生产者对counter的加1操作(counter++)对应机器指令:register1=counter; register1=register1+1; counter=register1;
  • 消费者对counter的减1操作(counter--)对应机器指令:register2=counter; register2=register2-1; counter=register2;
  • 若二者并发执行,可能出现指令交错(如生产者读取counter=5后,消费者也读取counter=5,最终counter可能被改为4而非5),导致counter值错误,进而引发缓冲池溢出或空取问题。

三、同步机制的四大规则

为确保临界区的安全访问,任何同步机制都需遵循以下4条核心规则,缺一不可:

规则名称 核心要求 目的
空闲让进 若临界区无进程执行,需访问临界区的进程应立即进入 避免资源空闲,提高资源利用率
忙则等待 若临界区已有进程执行,其他请求访问的进程必须等待 保证临界资源的互斥访问,防止冲突
有限等待 请求访问临界区的进程需在有限时间内进入,不得“永久等待” 避免“死等”(饥饿),确保进程公平性
让权等待 进程无法进入临界区时,需立即释放CPU(转为阻塞状态),而非“忙等” 避免CPU资源浪费,提高系统吞吐量

四、进程同步的实现机制

根据实现层面的不同,同步机制分为硬件同步机制软件同步机制(信号量、管程),其中硬件机制是基础,软件机制是主流应用方式。

1. 硬件同步机制

利用硬件指令的原子性(不可中断)实现互斥,无需操作系统内核干预,但存在局限性,适用于简单场景。

(1)关中断(Disable Interrupts)

  • 实现逻辑:进程进入临界区前关闭CPU中断,执行完临界区代码后再打开中断。关闭中断期间,CPU不响应任何中断请求,不会发生进程切换,确保临界区代码连续执行。
  • 优点:实现最简单,无需复杂数据结构。
  • 缺点
    1. 滥用关中断可能导致系统异常(如忽略硬件故障中断);
    2. 关中断时间过长会降低CPU利用率,限制并发能力;
    3. 不适用于多CPU系统(一个CPU关中断无法阻止其他CPU访问临界区)。

(2)Test-and-Set(TS)指令

  • 指令功能:原子性完成“测试锁状态”和“设置锁为占用”操作,返回锁的原始状态。指令定义如下:
    1
    2
    3
    4
    5
    boolean TS(boolean *lock){ 
    boolean old = *lock; // 保存锁的原始状态
    *lock = true; // 将锁设为“占用”
    return old; // 返回原始状态(true表示锁已占用,false表示空闲)
    }
  • 实现互斥的逻辑
    1
    2
    3
    4
    5
    6
    do{
    while(TS(&lock)); // 循环测试锁,直到锁空闲(TS返回false)
    临界区; // 锁空闲,进入临界区
    lock = false; // 退出临界区,释放锁
    剩余区;
    }while(TRUE);
  • 优点:支持多CPU系统,实现简单。
  • 缺点:未遵循“让权等待”规则,进程等待时处于“忙等”状态,浪费CPU资源。

(3)Swap指令(Exchange指令)

  • 指令功能:原子性交换两个变量的值,确保交换过程不被中断。指令定义如下:
    1
    2
    3
    4
    5
    void Swap(boolean *a, boolean *b){ 
    boolean temp = *a;
    *a = *b;
    *b = temp;
    }
  • 实现互斥的逻辑
    1
    2
    3
    4
    5
    6
    7
    8
    9
    do{
    boolean key = true;
    do{
    Swap(&lock, &key); // 交换lock和key的值,测试锁状态
    }while(key != false); // key为false表示锁空闲,退出循环
    临界区; // 进入临界区
    lock = false; // 释放锁
    剩余区;
    }while(TRUE);
  • 优缺点:与TS指令类似,支持多CPU但存在“忙等”问题。

2. 信号量机制(软件同步机制)

1965年由荷兰学者Dijkstra提出,是目前应用最广泛的同步机制之一。信号量是一个抽象数据结构,通过Pwait)、Vsignal)两个原子操作控制资源访问,可实现互斥与同步。

(1)信号量的核心属性

  • 本质:一个记录型数据结构,包含“资源数量”和“等待进程队列”(仅记录型信号量有队列);
  • 值的含义:信号量的值与资源使用状态相关,仅能通过PV操作修改;
  • 初始化:初值需在创建时设置一次,且初值非负(资源数量或同步标记)。

(2)信号量的分类与实现

根据功能和结构,信号量分为整型信号量记录型信号量AND型信号量信号量集,分别适用于不同场景:

① 整型信号量
  • 定义:信号量为整型变量S,仅通过PV原子操作访问:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // P操作(申请资源):若资源不足则“忙等”
    wait(S){
    while(S <= 0); // 资源不足时循环等待(忙等)
    S--; // 申请成功,资源数量减1
    }
    // V操作(释放资源):资源数量加1
    signal(S){
    S++; // 释放资源,资源数量加1
    }
  • 优点:实现简单,适用于单CPU系统。
  • 缺点:未遵循“让权等待”规则,进程等待时“忙等”,浪费CPU资源。
② 记录型信号量(解决“忙等”问题)
  • 定义:在整型信号量基础上增加“等待进程队列”,当资源不足时,进程进入队列阻塞(释放CPU),遵循“让权等待”规则。结构定义如下:
    1
    2
    3
    4
    typedef struct{
    int value; // 资源数量(value>=0:可用资源数;value<0:等待进程数的绝对值)
    struct PCB *list; // 等待该信号量的进程队列(PCB链表)
    }semaphore;
  • P、V操作实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // P操作(申请资源)
    wait(semaphore *S){
    S->value--; // 申请资源,资源数减1
    if(S->value < 0){ // 资源不足,进程阻塞
    block(S->list); // 将当前进程插入等待队列,释放CPU
    }
    }
    // V操作(释放资源)
    signal(semaphore *S){
    S->value++; // 释放资源,资源数加1
    if(S->value <= 0){ // 存在等待进程
    wakeup(S->list); // 唤醒等待队列的队首进程,插入就绪队列
    }
    }
  • 核心优势:无“忙等”,兼顾资源利用率与公平性,是目前主流的信号量形式。
  • 典型应用
    • 互斥信号量:初值为1,用于实现临界资源互斥访问(如mutex=1P(mutex)进入临界区,V(mutex)退出);
    • 资源信号量:初值为资源总数(如缓冲池空缓冲区数empty=n),用于同步资源访问;
    • 同步信号量:初值为0,用于实现进程间前趋关系(如进程A执行完S1后,通过V(S)唤醒进程B执行S2)。
③ AND型信号量(解决多资源申请死锁)
  • 背景:当进程需同时申请多个临界资源(如“哲学家进餐”问题中需同时拿两根筷子)时,若按顺序申请可能导致死锁(如每个哲学家都拿左筷子,再等右筷子)。
  • 核心思想:采用“原子操作”一次性申请所有所需资源,要么全部分配,要么一个不分配,避免部分资源占用导致的死锁。
  • 操作定义
    • Swait(S1, S2, ..., Sn):同时申请S1~Sn资源,所有资源可用时才分配;
    • Ssignal(S1, S2, ..., Sn):同时释放S1~Sn资源,唤醒等待进程。
  • 示例:哲学家申请筷子时,通过Swait(chopstick[i], chopstick[(i+1)%5])同时申请左右筷子,避免死锁。
④ 信号量集(批量资源申请)
  • 背景:记录型信号量每次仅能申请1个资源,若需批量申请(如一次申请3个缓冲区),需多次执行P操作,效率低且可能引发死锁。
  • 核心思想:支持一次申请多个资源,并可设置“资源分配下限”(不足下限不分配),操作定义如下:
    1
    2
    3
    4
    5
    Swait(S1, t1, d1, ..., Sn, tn, dn):
    - 对每个信号量Si,若Si >= ti(ti为分配下限),则分配di个资源(Si = Si - di);
    - 否则,进程阻塞。
    Ssignal(S1, d1, ..., Sn, dn):
    - 对每个信号量Si,释放di个资源(Si = Si + di),唤醒等待进程。
  • 特殊场景
    • Swait(S, d, d):一次申请d个资源,不足d个则等待;
    • Swait(S, 1, 1):等同于记录型信号量(互斥或单个资源申请);
    • Swait(S, 1, 0):仅测试资源状态(S>=1时允许进入,S=0时禁止),可作为“可控开关”。

(3)信号量的典型应用

信号量可灵活实现进程互斥进程同步(前趋关系)复杂协作场景,以下为常见应用案例:

① 实现进程互斥
  • 逻辑:为临界资源设置互斥信号量mutex(初值1),进程进入临界区前执行P(mutex)(申请资源),退出时执行V(mutex)(释放资源)。
  • 示例:两个进程PA、PB互斥访问打印机:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    semaphore mutex = 1;  // 互斥信号量,初值1
    void PA(){
    while(1){
    P(mutex); // 申请打印机
    临界区(使用打印机);
    V(mutex); // 释放打印机
    剩余区;
    }
    }
    void PB(){
    while(1){
    P(mutex); // 申请打印机
    临界区(使用打印机);
    V(mutex); // 释放打印机
    剩余区;
    }
    }
  • 注意PV操作必须成对出现,缺P会导致互斥失效,缺V会导致资源永久占用。
② 实现进程前趋关系
  • 逻辑:为每个前趋关系设置同步信号量(初值0),前趋操作执行完后V操作(唤醒后续进程),后续操作执行前P操作(等待前趋完成)。
  • 示例:进程P1的S1执行后,P2的S2和P3的S3才能执行:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    semaphore a = 0, b = 0;  // 同步信号量,初值0
    void P1(){
    S1;
    V(a); // S1完成,唤醒P2
    V(b); // S1完成,唤醒P3
    }
    void P2(){
    P(a); // 等待S1完成
    S2;
    }
    void P3(){
    P(b); // 等待S1完成
    S3;
    }
③ 案例1:过独木桥问题
  • 需求:同一方向行人可连续过桥,反向行人需等待当前方向无人后才能过桥。
  • 实现:设置互斥信号量mutex(控制桥的独占)、方向计数信号量leftm/rightm(保护方向计数器),通过计数器判断是否需申请桥资源:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    semaphore mutex = 1, leftm = 1, rightm = 1;
    int countleft = 0, countright = 0; // 左右方向过桥人数
    // 左→右过桥
    void leftToRight(){
    P(leftm); // 保护countleft
    if(countleft == 0) // 第一个左→右行人,申请桥
    P(mutex);
    countleft++; // 左→右人数加1
    V(leftm); // 释放countleft

    过桥; // 临界区

    P(leftm); // 保护countleft
    countleft--; // 左→右人数减1
    if(countleft == 0) // 最后一个左→右行人,释放桥
    V(mutex);
    V(leftm); // 释放countleft
    }
    // 右→左过桥逻辑类似,操作countright和rightm
④ 案例2:父子四人水果问题
  • 需求:父亲放苹果、母亲放桔子,儿子吃桔子、女儿吃苹果,盘子仅能放1个水果。
  • 实现:设置信号量empty(盘子空,初值1)、apple(苹果数量,初值0)、orange(桔子数量,初值0)、mutex(保护盘子操作):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    semaphore mutex = 1, apple = 0, orange = 0, empty = 1;
    // 父亲放苹果
    void father(){
    P(empty); // 申请空盘子
    P(mutex); // 保护盘子操作
    放苹果;
    V(mutex); // 释放盘子
    V(apple); // 通知女儿有苹果
    }
    // 母亲放桔子(类似父亲,操作empty和orange)
    // 女儿吃苹果(P(apple)申请苹果,V(empty)释放盘子)
    // 儿子吃桔子(类似女儿,操作orange和empty)

3. 管程机制(封装式同步机制)

信号量机制需在每个进程中手动编写PV操作,同步逻辑分散且易出错(如PV顺序颠倒)。管程机制通过“封装共享资源和操作”,实现同步逻辑的集中管理,确保每次仅一个进程进入管程,简化同步实现。

(1)管程的定义与结构

  • 定义:管程是一个抽象数据类型,包含“共享数据结构”、“操作共享数据的过程”、“条件变量”和“初始化代码”,进程需通过管程提供的过程访问共享资源。
  • 核心特性互斥性——管程确保每次仅一个进程执行其内部过程,无需进程手动实现互斥;同步性——通过条件变量实现进程间的协作等待与唤醒。
  • 结构组成
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Monitor 管程名{
    共享变量声明; // 管程管理的共享资源(如缓冲池、计数器)
    条件变量声明; // 用于进程同步的条件变量(如notfull、notempty)
    public:
    过程1(参数){ ... } // 访问共享资源的过程(如put、get)
    过程2(参数){ ... } // 访问共享资源的过程
    ...
    初始化代码; // 初始化共享变量和条件变量
    }

(2)条件变量(管程中的同步工具)

管程通过条件变量实现进程间的同步等待与唤醒,条件变量仅能在管程内部使用,提供waitsignal两个操作:

  • 条件变量的作用:当进程通过管程申请资源失败时,通过wait进入条件队列阻塞(释放管程);当资源可用时,其他进程通过signal唤醒阻塞进程。
  • 核心操作
    • x.wait():当前进程因x条件阻塞,插入x的等待队列,释放管程,允许其他进程进入;
    • x.signal():当前进程触发x条件,唤醒x等待队列的一个进程(若队列非空);若唤醒后存在进程竞争,需按规则处理(如唤醒进程优先执行或当前进程继续执行)。

(3)管程的优势

  • 封装性:同步逻辑集中在管程内部,进程无需关注细节,降低编程复杂度;
  • 安全性:管程自动实现互斥,避免PV操作遗漏或顺序错误导致的同步问题;
  • 可维护性:同步逻辑修改仅需调整管程内部,无需修改所有相关进程。

五、核心总结

进程同步的本质是解决“并发导致的资源争夺与协作混乱”,其核心逻辑可归纳为:

  1. 识别临界资源与临界区:明确需互斥访问的资源及对应代码段,是同步设计的基础;
  2. 选择合适的同步机制:硬件机制适用于简单场景,信号量机制灵活通用(互斥、同步、多资源),管程机制适用于复杂共享资源管理;
  3. 遵循同步规则:确保同步机制满足“空闲让进、忙则等待、有限等待、让权等待”,兼顾效率与公平性;
  4. 典型应用场景:通过信号量或管程可解决“互斥访问”“前趋关系”“多资源协作”等问题,需结合具体需求设计同步逻辑(如“生产者-消费者”“哲学家进餐”等经典问题)。

进程同步
https://hellowydwyd.github.io/2025/10/16/进程同步/
作者
YuDong Wang
发布于
2025年10月16日
许可协议