管程

管程的原理与操作过程

1. 为什么需要管程?

在多进程并发环境中,为了保证数据的一致性和正确性,需要对共享资源进行访问控制。传统的信号量机制虽然可以实现互斥和同步,但信号量操作(P/V操作)分散在各个进程中,这使得程序的正确性难以验证,容易出现以下问题:

为了解决这些问题,Hoare 和 Brinch Hansen 提出了管程的概念。管程将共享数据及其所有对这些数据进行操作的过程(函数)封装在一个模块中,提供了一种更高级别的同步机制。

2. 管程的组成

管程是一个由共享数据结构一组操作这些数据结构的函数(过程)组成的模块。它通常包括:

3. 管程的特性

管程的核心特性是一次只允许一个进程在管程中执行。这确保了对共享数据的互斥访问。具体来说:

4. 管程实现互斥和同步的过程

  1. 进入管程:进程通过调用管程的某个过程来请求进入管程。

  2. 获取互斥权:如果管程当前没有其他进程执行,则该进程获得管程的互斥权并进入管程执行其过程。如果管程当前有其他进程执行,则该进程在管程的入口处等待。

  3. 执行过程:进程在管程内执行操作共享数据的过程。

  4. 条件不满足时等待:如果在执行过程中发现某个条件不满足(例如,消费者发现缓冲区为空),进程会调用 wait(condition) 操作。此时,该进程会被阻塞,加入到对应的条件变量等待队列中,并释放管程的互斥权,以便其他进程可以进入管程。

  5. 条件满足时唤醒:当其他进程进入管程并修改了共享数据,使得某个条件满足(例如,生产者向缓冲区放入数据),它会调用 signal(condition) 操作。如果该条件变量的等待队列中有阻塞进程,则唤醒其中一个进程。

  6. 离开管程:进程执行完管程内部的过程,或者被唤醒后继续执行并完成操作,就会离开管程并释放互斥权。

条件变量的两种语义(了解,常作为选择题考点)

对于 signal 操作唤醒一个等待进程时的处理方式,主要有两种不同的语义:


示例:生产者-消费者问题

这里我们使用管程来解决经典的生产者-消费者问题。

问题描述:有一个固定大小的缓冲区,生产者向缓冲区中放入数据,消费者从缓冲区中取出数据。要求生产者和消费者并发执行,但不能同时访问缓冲区,且生产者不能在缓冲区满时生产,消费者不能在缓冲区空时消费。

代码段

monitor ProducerConsumer {
    // 共享数据
    item buffer[N];      // 缓冲区,大小为 N
    int count = 0;       // 缓冲区中当前数据项的数量
    int in = 0;          // 生产者下次放入数据的位置
    int out = 0;         // 消费者下次取出数据的位置

    // 条件变量
    condition notFull;   // 缓冲区不满的条件
    condition notEmpty;  // 缓冲区不空的条件

    // 生产者放入数据
    procedure insert(item x) {
        if (count == N) { // 缓冲区已满
            wait(notFull); // 阻塞并等待 notFull 条件
        }
        buffer[in] = x;
        in = (in + 1) % N;
        count++;
        signal(notEmpty);  // 唤醒等待 notEmpty 条件的消费者
    }

    // 消费者取出数据
    procedure remove() returns item {
        if (count == 0) { // 缓冲区为空
            wait(notEmpty); // 阻塞并等待 notEmpty 条件
        }
        item x = buffer[out];
        out = (out + 1) % N;
        count--;
        signal(notFull);   // 唤醒等待 notFull 条件的生产者
        return x;
    }

    // 初始化代码(可选,有些语言自动初始化)
    // init() {
    //    count = 0;
    //    in = 0;
    //    out = 0;
    // }
}

// 生产者进程
producer() {
    while (true) {
        item newItem = produce_item();
        ProducerConsumer.insert(newItem); // 调用管程的 insert 过程
    }
}

// 消费者进程
consumer() {
    while (true) {
        item consumedItem = ProducerConsumer.remove(); // 调用管程的 remove 过程
        consume_item(consumedItem);
    }
}

图示解析(概念性)

+------------------------------------+
|         生产者-消费者问题           |
|                                    |
| 共享数据: 缓冲区[], 计数器, 入, 出   |
|                                    |
| 过程:                               |
|    - 插入(项目 x)                   |
|    - 移除() 返回项目                 |
|                                    |
| 条件变量:                           |
|    - notFull (缓冲区满时的等待队列)    |
|    - notEmpty (缓冲区空时的等待队列)   |
|                                    |
| [隐式互斥锁]                       |
+------------------------------------+
             ^       ^       ^
             |       |       |
      生产者进程    消费者进程 (每次只有一个进程在内部)

工作流程简述

  1. 当生产者尝试 insert 数据时,如果 count == N(缓冲区满),则它会在 notFull 条件变量上等待,同时释放管程的互斥权

  2. 当消费者尝试 remove 数据时,如果 count == 0(缓冲区空),则它会在 notEmpty 条件变量上等待,同时释放管程的互斥权

  3. 当生产者成功 insert 一个数据后,它会 signal(notEmpty),如果此时有消费者在 notEmpty 上等待,则唤醒它。

  4. 当消费者成功 remove 一个数据后,它会 signal(notFull),如果此时有生产者在 notFull 上等待,则唤醒它。