您的当前位置:首页正文

管程的理解

2025-01-16 来源:个人技术集锦


什么是管程

管程的概念

1.是一种程序结构,结构内的多个子程序(对象或模块)形成的多个工作线程互斥访问共享资源。这些共享资源一般是硬件设备或一群变量。管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。

2.与那些通过修改数据结构实现互斥访问的并发程序设计相比,管程实现很大程度上简化了程序设计。

3.管程提供了一种机制,线程可以临时放弃互斥访问,等待某些条件得到满足后,重新获得执行权恢复它的互斥访问。即:在管程中的线程可以临时放弃管程的互斥访问,让其他线程进入到管程中来。

4.管程包含:

  •                  多个彼此可以交互并共用资源的线程
  •                  多个与资源使用有关的变量
  •                  一个互斥锁
  •                  一个用来避免竞态条件的不变量

1.一个管程的程序在运行一个线程前会先取得互斥锁,直到完成线程或是线程等待某个条件被满足才会放弃互斥锁。若每个执行中的线程在放弃互斥锁之前都能保证不变量成立,则所有线程皆不会导致竞态条件成立。

2.管程是一种高级的同步原语。任意时刻管程中只能有一个活跃进程。它是一种编程语言的组件,所以编译器知道它们很特殊,并可以采用与其他过程调用不同的方法来处理它们。典型地,当一个进程调用管程中的过程,前几条指令将检查在管程中是否有其他的活跃进程。如果有,调用进程将挂起,直到另一个进程离开管程。如果没有,则调用进程便进入管程。

3.对管程的实现互斥由编译器负责!在Java中,只要将关键字synchronized加入到方法声明中,Java保证一旦某个线程执行该方法,就不允许其他线程执行该方法,就不允许其他线程执行该类中的任何其他方法。

4.注意:管程是一个编程语言概念。编译器必须要识别出管程并用某种方式对互斥做出安排。C、Pascal及多数其他语言都没有管程,所以指望这些编译器来实现互斥规则是不可靠的。

5.管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制

6.进程只能互斥得使用管程,即当一个进程使用管程时,另一个进程必须等待。当一个进程使用完管程后,它必须释放管程并唤醒等待管程的某一个进程。

7. 在管程入口处的等待队列称为入口等待队列,由于进程会执行唤醒操作,因此可能有多个等待使用管程的队列,这样的队列称为紧急队列,它的优先级高于等待队列。


管程的特征


1.     模块化。

管程是一个基本的软件模块,可以被单独编译。

2.     抽象数据类型。

管程中封装了数据及对于数据的操作,这点有点像面向对象编程语言中的类。

3.     信息隐藏。

管程外的进程或其他软件模块只能通过管程对外的接口来访问管程提供的操作,管程内部的实现细节对外界是透明的。

4.     使用的互斥性。

任何一个时刻,管程只能由一个进程使用。进入管程时的互斥由编译器负责完成。

管程的功能 

管程本身是一个进程同步这样的机制,因此它具有互斥与同步的功能

互斥

>管程中的变量只能被管程中的操作访问

>任何时候只有一个进程在管程中操作类似临界区

>类似临界区

>由编译器完成 

同步

>条件变量

>唤醒和阻塞操做 

管程的组成


管程,你可以把它当作一个类看作,一个类里面有啥? 在 Java 类里面可以有属性、构造方法、业务方法,管程它里面也有这些东西。

1、局部于管程的共享变量,这里就可以理解类里面的属性。

2、对数据结构进行操作的一组过程,这里可以理解成业务方法,在方法里面我们可以操作类属性。

3、对局部于管程的数据进行初始化的语句,可以理解为构造方法,初始化属性值。

而且管程还有这么几个特性:

1、管程内的数据,只能被管程里面的方法所访问,管程内部的共享变量对外是不能直接访问的。

2、一个进程只能调用管程中的方法,才能访问管程内的共享数据。

3、每一次,只允许一个进程调用管程内的某个方法。
 

enter过程、leave过程、条件型变量c、wait(c) 、signal(c)


1.     enter过程

一个进程进入管程前要提出申请,一般由管程提供一个外部过程--enter过程。如Monitor.enter()表示进程调用管程Monitor外部过程enter进入管程。

2.     leave过程

当一个进程离开管程时,如果紧急队列不空,那么它就必须负责唤醒紧急队列中的一个进程,此时也由管程提供一个外部过程—leave过程,如Monitor.leave()表示进程调用管程Monitor外部过程leave离开管程。

3.     条件型变量c

条件型变量c实际上是一个指针,它指向一个等待该条件的PCB队列。如notfull表示缓冲区不满,如果缓冲区已满,那么将要在缓冲区写入数据的进程就要等待notfull,即wait(notfull)。相应的,如果一个进程在缓冲区读数据,当它读完一个数据后,要执行signal(notempty),表示已经释放了一个缓冲区单元。

4.     wait(c)

wait(c)表示为进入管程的进程分配某种类型的资源,如果此时这种资源可用,那么进程使用,否则进程被阻塞,进入紧急队列。

5.     signal(c)

signal(c)表示进入管程的进程使用的某种资源要释放,此时进程会唤醒由于等待这种资源而进入紧急队列中的第一个进程。
 

管程可能存在的处理方式 

管程内可能存在不只一个进程,比如:进程P调用 signal() 唤醒进程Q。 

1.>P等待,直到Q离开管程或等待另一条件 (Hoare)

Hoare 管程是由 Tony Hoare 提出的一种管程实现模型。这种模型主要借助条件变量(Condition Variables)实现线程同步。在 Hoare 管程模型中,一旦一个线程执行了条件变量的 signal(发出唤醒信号)操作,那么正在等待这个条件变量的线程将立即与发出唤醒信号的线程共享 CPU。换句话说,waiting 线程会立即被唤醒并执行。因此,Hoare 管程可能导致较高的线程切换次数。

2.>Q等待,直到P离开管程或等待另一条件 (Hansen) 

Brinch-Hansen管程(也称为 Mesa 管程)是由 Per Brinch-Hansen 提出的一种管程实现策略。与 Hoare 管程不同,Brinch-Hansen 管程在唤醒等待条件变量的线程时转换了线程调度模型。在 Brinch-Hansen 管程中,当一个线程执行条件变量的 signal(发出唤醒信号)操作时,并不会立即调度等待线程,而是将唤醒的线程添加到调度队列中。在发出唤醒信号的线程释放管程锁之后,调度器才可能会选择被唤醒的线程。这种策略降低了线程切换的频率,从而提高了性能。

Java实现 

// Java 中管程的简单实现

public class MonitorExample {
  private final Object lock = new Object(); // 锁

  void method() {
    synchronized(lock) {
      while(/* condition */) {
        try {
          lock.wait();
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
          // 处理中断
        }
      }
      // 临界区代码
      lock.notify();  // 在 Hoare 管程中, 被 notify 的线程会立即运行
                     // 在 Brinch-Hansen 管程中, 被 notify 的线程会被添加到就绪队列,待到 lock 释放后再被调度
    }
  }
}

区别 

两者之间的主要区别在于线程唤醒的调度策略。在 Hoare 管程中,唤醒的线程立即得到执行,可能导致较高的线程切换次数;而在 Brinch-Hansen 管程中,唤醒的线程被添加到调度队列,延迟了线程的执行,从而提高了性能。根据实际应用场景和需求,可以选择适合的管程实现策略。

Hoare 看上去直观点,但是实现起来比较困难,主要见于教材中

Brinch-Hansen 实现起来相对比较简单,在很多操作系统里面就是这么实现的,方法1实现起来需要更复杂的机制才能保证实现方法的有效性。 主要用于真实OS和JAVA中

管程实现互斥、同步 

完整代码模拟管程实现。 

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

#define BUFFER_SIZE 10 // 缓冲区大小

// 定义监控器结构体
typedef struct monitor {
  int buffer[BUFFER_SIZE]; // 缓冲区
  int in_index; // 生产者处理索引
  int out_index; // 消费者处理索引
  int count; // 缓冲区满/空的计数
  pthread_mutex_t mutex; // 互斥锁,保证缓冲区的线程安全
  pthread_cond_t full; // 缓冲区满的条件变量
  pthread_cond_t empty; // 缓冲区空的条件变量
} Monitor;

// 初始化监控器
void monitor_init(Monitor* mon){
  mon->in_index = 0;
  mon->out_index = 0;
  mon->count = 0;
  pthread_mutex_init(&mon->mutex, NULL);
  pthread_cond_init(&mon->full, NULL);
  pthread_cond_init(&mon->empty, NULL);
}

// 销毁监控器
void monitor_destroy(Monitor* mon){
  pthread_mutex_destroy(&mon->mutex);
  pthread_cond_destroy(&mon->full);
  pthread_cond_destroy(&mon->empty);
}

// 生产者
void monitor_produce(Monitor* mon, int value){
  pthread_mutex_lock(&mon->mutex); // 加锁
  while (mon->count == BUFFER_SIZE){ // 缓冲区满,则等待
    pthread_cond_wait(&mon->full, &mon->mutex);
  }

  mon->buffer[mon->in_index] = value; // 存入数据
  mon->in_index = (mon->in_index + 1) % BUFFER_SIZE; // 处理索引移动
  mon->count++; // 计数增加

  pthread_cond_signal(&mon->empty); // 唤醒等待“空”条件的线程
  pthread_mutex_unlock(&mon->mutex); // 解锁
}

// 消费者
int monitor_consume(Monitor* mon){
  pthread_mutex_lock(&mon->mutex); // 加锁
  while (mon->count == 0){ // 缓冲区空,则等待
    pthread_cond_wait(&mon->empty, &mon->mutex);
  }

  int value = mon->buffer[mon->out_index]; // 取出数据
  mon->out_index = (mon->out_index + 1) % BUFFER_SIZE; // 处理索引移动
  mon->count--; // 计数减少

  pthread_cond_signal(&mon->full); // 唤醒等待“满”条件的线程
  pthread_mutex_unlock(&mon->mutex); // 解锁

  return value; // 返回取出的数据
}

Monitor counter_monitor; // 定义监控器

// 生产者线程函数
void* producer(void* arg){
  int i;
  for (i = 0; i < 100; i++){
    monitor_produce(&counter_monitor, i); // 生产并存入数据
    printf("Produced %d\n", i); // 打印生产了哪个数据
  }

  pthread_exit(NULL); // 退出线程
}

// 消费者线程函数
void* consumer(void* arg){
  int value;
  while (1){
    value = monitor_consume(&counter_monitor); // 取出并消费数据
    printf("Consumed %d\n", value); // 打印消费了哪个数据
  }

  pthread_exit(NULL); // 退出线程
}

int main(){
  pthread_t threads[2];
  monitor_init(&counter_monitor); // 初始化监控器

  pthread_create(&threads[0], NULL, producer, NULL); // 创建生产者线程
  pthread_create(&threads[1], NULL, consumer, NULL); // 创建消费者线程

  pthread_join(threads[0], NULL); // 等待生产者线程结束
  pthread_join(threads[1], NULL); // 等待消费者线程结束

  monitor_destroy(&counter_monitor); // 销毁监控器
  return 0;
}

如果实现通信问题,采用记录型信号量的方式时,同时存在互斥和同步信号量,wait()操作一定要同步的在前,互斥的在后,signal操作则无所谓。

Top