操作系统『8』信号量和管程


    互斥无法解决同步问题。所以引入信号量、管程的概念。

一、信号量(Semaphore)

    信号量可以分为两种:一种是二进制信号量:资源数目为 0 或 1;另一种是资源信号量:资源数目为任何非负值。两者是等价的,基于一个可以实现另一个。
    信号量是有符号整数,是被保护的变量,针对信号量有两个操作,P操作和 V 操作。信号量一般初始化为大于 0 的数,这样的话 P 操作才不会被阻塞。
    信号量可用于临界区的互斥访问控制 和 线程间的事件等待、条件同步。

1.用信号量实现临界区的互斥访问
  • 用二进制信号量实现互斥访问:
//每个临界区设置一个信号量,其初始值为 1
mutex=new Semaphore(1);
mutex->P();
Critical Section;
mutex->V();

P():
     对 sem 减 1,如果 sem<0 ,就等待,否则继续往下执行。P 操作能够阻塞。
Y():
    对 sem 加 1,如果 sem<=0,意味着有线程在等待,就会唤醒一个或多个等待的 P 线程。Y 操作不会阻塞。
    必须成对使用 P() 操作和 V() 操作,P() 操作保证互斥访问临界资源,V() 在使用后释放邻居资源,PV 操作不难次序错误、重复 或 遗漏。
    (经常是 FIFO 的形式。)

  • 用二进制信号量实现调度约束——同步操作:线程 A 需要线程 B 执行完某条语义之后才继续执行。
//初始值设置为 0 
condition=new Semaphore(0);

Thread A:

condition -> P();

Thread B:

conditon-> V();

👀例:有界缓冲区的生产者-消费者问题
在这里插入图片描述
要求:
    一个或多个生产者将数据放在一个缓冲区里,的那个消费者每次从缓冲区取出数据,在任何时候只有一个生产者或消费者线程可以访问该缓冲区 。即 双方需要同时用到同步和互斥两种机制:
① 在任何时候只有一个生产者或消费者线程可以访问该缓冲区 。(互斥)
② 当缓冲区为空时,消费者必须等待生产者。(调度/同步约束)
③ 当缓存区满,生产者必须等待消费者。(调度/同步约束)
    
方案:
    每个约束用一个单独的信号量。
    使用二进制信号量mutex ;
    一般信号量 fullBuffers;
    一般信号量 emptyBuffers。

Class BoundedBuffer {
    mutex = new Semaphore(1);
    fullBuffers = new Semaphore(0);
    //初始值设置为 n,这样就可以有 n 个进入或者说可以进入 n 次
    emptyBuffers = new Semaphore(n);
}

生产者:

//生产者
BoundedBuffer::Deposit(c) {

    emptyBuffers->P();
    
    /*通过 前加 P 后加 V 实现互斥*/
    mutex->P(); 
    
    //向 Buffer 添加数据
    Add c to the buffer;
    
    mutex->V();
     /*通过 前加 P 后加 V 实现互斥*/
     
    fullBuffers->V();
    
}

消费者:

//消费者
BoundedBuffer::Remove(c) {

    fullBuffers->P();
    
    mutex->P();
    
    //从 Buffer 取出数据
    Remove c from buffer;
    
    mutex->V();
    
    emptyBuffers->V();
}

(其实这个和 Java里面的实现也蛮像的。)


2.信号量的实现

    信号量本身是整型,所以需要一个整型记录信号量加加减减的值,P 操作时可能有进程要在信号量上进行等待,所以需要一个等待队列

class Semaphore
 {
  int sem;
  WaitQueue q;
 }

P操作:

Semaphore: P()
 {
 sem--;
 if(sem<0)
  //将进程放入等待队列,并进入睡眠
  Add this thread to q;
  block(p);
 }

V操作:

Semaphore: V()
 {
  sem++;
  //判断是否有进程在睡眠
  if(sem<=0)
  //从等待队列取出一个(等待时间最久的)进程
  Remove this thread from q;
  wekeup(p);
 }

    信号量除了以上互斥之外,还可以用在 条件同步
,而且使用了等待队列,所以和 锁的 “忙等” 是有区别的,是可以睡眠的。而信号量存在一个问题——不难处理死锁。

二、管程(Moniter)

    管程主要是针对语言的并发情况提出的。
    管程是一种用于多线程互斥访问共享资源的程序结构,采用面向对象方法,简化了线程间的同步控制,任一时刻最多只有一个线程执行管程代码,正在管程中的线程可临时放弃管程的互斥访问,等待事件出现时恢复。
    管程包含一个锁🔒(指定临界区,保证互斥) 和 0 或 多个条件变量 (将等待的线程进行挂起)。

在这里插入图片描述

(像极了 Java 里的 wait 和 notify ☕)

  • Lock 锁🔒
    Lock :: Acquire() :等待直到锁可用,然后抢占锁。
    Lock :: Release() :释放锁,唤醒等待者。

  • Condition Variable 条件变量:

Class Condition {
    int numWaiting = 0;
    WaitQueue q;
}

(1)Wait operation
    释放锁,然后睡眠。

Condition::Wait(lock){
    numWaiting++;
    Add this thread t  to q;
    //还没获得呢,可先释放了。
    release(lock);
    //选择下一个线程
    schedule(); //need mutex
    require(lock);
}

这里先 release 再 require 了。

(2)Signal() operation (or backcast() operation)
    当某些条件得到满足了,唤醒等待者。

Condition::Signal(){
    if (numWaiting > 0) {
        Remove a thread t from q;
        //唤醒线程
        wakeup(t); //need mutex
        numWaiting--;
    }
}

👀例:有界缓冲区的生产者-消费者问题

classBoundedBuffer {
    …
    Lock lock;
    
    //记录 buffer 当前空闲情况,
    //如果 count 为 0 表示 buffer 为空;
    //如果 count 为 1 表示 buffer 为满。
    int count = 0;
    
    Condition notFull, notEmpty;
}

生产者:

BoundedBuffer::Deposit(c) {

   /*进入管程的线程是唯一的互斥的*/
    lock->Acquire();
    
    while (count == n)
       //count 增加到 n 表示 buffer 满啦
       //进入睡眠
        notFull.Wait(&lock);
    Add c to the buffer;
    count++;
    notEmpty.Signal();
    
    /*进入管程的线程是唯一的互斥的*/
    lock->Release();
}

消费者:

BoundedBuffer::Remove(c) {
    lock->Acquire();
    while (count == 0)    
      notEmpty.Wait(&lock);
    Remove c from buffer;
    count--;
    notFull.Signal();
    lock->Release();
}

    线程在管程中执行时,如果某个线程要执行某个条件变量的唤醒 signal 操作时,执行完之后是马上执行等待在条件变量上的线程,(Hoare)还是执行完 调用了 signal 操作的这个线程之后(Hansen),再去执行等待的线程。(因为调用了 signal 之后,实际上有两个线程是可以执行的,一个是等待着的,一个就是调用了 signal 的这个,到底选择哪个进程先执行,这 是个问题。感觉之前接触到的不会等这个线程执行完的,就直接去执行等待着的。😥)

Hansen 管程与 Hoare 管程

在这里插入图片描述
对比俩的代码(前面的例子里用的就是 Hansen-while ):

Hansen-style :Deposit(){
  lock->acquire();
  while (count == n) {
       notFull.wait(&lock); 
  }
  Add  thing;
  count++;
  notEmpty.signal();
  lock->release();
}
Hoare-style: Deposit(){
  lock->acquire();
  if (count == n) {
       notFull.wait(&lock); 
  }
  Add thing;
  count++;
  notEmpty.signal();
  lock->release();
}

    主要是用 while 还是 if 的区别,Hansen 执行完 signal 之后,并没有马上执行等待着的线程,必须要继续往下执行,count++、release 锁释放,然后,可能有多个等待在条件变量的线程也被唤醒 (???这块没懂) 有可能存在多个被唤醒的线程,大家都会去抢占 CPU,然后只能有一个线程被选中,所以线程被选中之后,count 不为 n 了,所以需要用 while 再确认,而用 Hoare 的话,调用了 signal 就去执行能够等待着的线程了,就唤醒了一个 ,count 值一定不为 n (因为做 signal 时是 count 小于 n 时)。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值