十、信号量与管程

信号量与管程

背景

并发问题:竞态条件

  • 多线程并发导致资源竞争

同步概念

  • 多线程共享公共数据的协调执行
  • 包括互斥与条件同步
  • 互斥:任何时刻只能有一个线程执行临界区代码

确保同步正确的方法

  • 底层硬件支持

  • 高层次的编程抽象(如,锁)

基本同步方法

信号量

希望临界区能有多个线程和进程执行,如读操作,希望能用更高级的同步互斥手段,通过信号量来实现

信号量是一种抽象数据类型

  • 由一个整形 (sem)变量和两个原子操作组成

  • P() :sem减1,如果sem<0,等待,否则继续

  • V() :sem加1,如果sem<=0,说明当前有等着的进程,唤醒挂在信号量上的等待进程,可以是一个,可以是多个

信号量与铁路的类比

  • 初始化2个资源控制的信号灯

信号量提出之后迅速被用在早起的OS中,如Unix,现在的OS也大量存在。

信号量的特性

  • 信号量是被保护整数变量:

  1. 初始化完成后,只能通过P()和V()操作修改
  2. 由操作系统保证,PV操作是原子操作

  • P() 可能阻塞,V()不会阻塞

  • 通常假定信号量是“公平的”

  1. 线程不会被无限期阻塞在P()操作

  2. 假定信号量等待按先进先出排队

信号量分类

可分为两种信号量

  • 二进制信号量:资源数目为0或1

  • 一般/计数信号量:资源数目为任何非负值

两者等价:基于一个可以实现另一个

信号量的使用用在两个方面

  • 互斥访问:临界区的互斥访问控制

  • 条件同步:调度约束——线程间的事件等待

用信号量实现临界区的互斥访问

每个临界区设置一个信号量,其初值为1


必须成对使用P()操作和V()操作

  • P()操作保证互斥访问临界资源

  • V()操作在使用后释放临界资源

PV操作不能次序错误、重复或遗漏

用信号量实现条件同步

每个条件同步设置一个信号量,其初值为0


P()等待,V()发出信号。

生产者-消费者问题:更复杂的情况


一个线程等待另一个线程处理事情:比如生产东西或者消费东西,互斥(锁机制是不够的),还需要条件同步。

有界缓冲区的生产者-消费者问题描述

  • 一个或多个生产者在生成数据后放在一个缓冲区里

  • 单个消费者从缓冲区取出数据处理

任何时刻只能有一个生产者或消费者可访问缓冲区

问题分析

  • 任何时刻只能有一个线程操作缓冲区(互斥访问)

  • 缓冲区空时,消费者必须等待生产者(调度/条件同步)

缓冲区满时,生产者必须等待消费者(调度/条件同步)

用信号量描述每个约束

  • 二进制信号量mutex

  • 计数信号量fullBuffers

计数信号量emptyBuffers


二进制信号量mutex初值为0,保证互斥。fullBuffers初值为0,表明Buffers初始为0。emptyBuffers初值为Buffers的size.

P、V操作的顺序有影响吗?生成者两个V交换没有问题,但P交换会造成死锁,P操作会产生阻塞,设置不当会造成死锁

信号量的实现

使用硬件原语:禁用中断、原子指令(test and set),类似锁

例如:使用禁用中断


需要一个整型变量sem和等待队列q。block(p),p睡眠。采用先进先出组织队列q.

信号量的双用途

  • 互斥和条件同步
  • 但是等待条件是独立的互斥

使用信号量的困难:

  • 读/开发代码比较困难:程序员需要非常精通信号量
  • 容易出错:使用的信号量已经被另一个线程占用,忘记释放信号量
  • 不能够处理死锁问题
OS中其实存在大量的信号量的使用。

管程Moniter

目的:分离互斥和条件同步的关注

什么是管程:管程是一种用于多线程互斥访问共享资源的程序结构

  • 采用面向对象方法,简化了线程间的同步控制

任一时刻最多只有一个线程执行管程代码

正在管程中的线程可临时放弃管程的互斥访问,等待事件出现时恢复

管程的使用

  • 在对象/模块中,收集相关共享数据

  • 定义访问共享数据的方法

管程的组成

  • 一个锁:控制管程代码的互斥访问

  • 0或者多个条件变量:管理共享数据的并发访问


条件变量(Condition Variable

条件变量是管程内的等待机制

  • 线程因资源被占用而进入等待状态

  • 每个条件变量表示一种等待原因,对应一个等待队列

Wait()操作

  • 释放锁,睡眠,重新获得锁返回

Signal()操作

  • 唤醒等待着(或者所有等待着),如果有。如果等待队列为空,则等同空操作(和信号量不一样)

条件变量实现


注意:numWaiting和信号量中的sem不一样,signal的numWaiting不一定会做减1操作。schedule选择下一个线程执行,自身睡眠。wakeup()将当前睡眠状态的线程重新置为ready状态,被调度处于运行时继续往下走。

用管程解决生产者-消费者问题


count记录Buffer的空闲状况,为0表示空,为n表示满。初始时为0。这里的互斥在头和尾,由管程的定义决定。

管程条件变量的释放处理方式

发出signal后的不同处理方式



对于方案一并没有马上让等待的线程被唤醒执行,有可能存在多个等待条件变量队列上的线程被唤醒,大家可能去抢占CPU执行,有可能被唤醒的线程被执行时count已经不为n。而方案二中被唤醒的进程马上执行。

基本同步方法


开发/调试并行程序很难:非确定性的交叉指令

同步结果:

  • 锁:互斥
  • 条件变量:有条件的同步
  • 其他原语:信号量
  • 怎样有效的使用这些结果?制定并遵循严格的程序设计、策略

经典同步问题

读者写者问题

动机:共享数据的访问

两种类型的使用者:

  • 读者:只读取数据,不修改

  • 写者:读取和修改数据

问题约束:

  • 同一时刻,允许有多个读者同时读   读读允许

  • 没有写者时读者才能读                    写读互斥

没有读者时写者才能写                    读写互斥

没有其他写者时写者才能写            写写互斥

信号量描述每个约束

  • 信号量WriteMutex,控制写操作的互斥,初始化为1

  • 读者计数Rcount,正在进行读操作的读者数目,初始化为0

信号量CountMutex,控制对读者计数的互斥修改,初始化为1,不加保护的话加或者减会出错。

读者优先策略

  • 只要有读者正在读状态,后来的读者都能直接进入

  • 如读者持续不断进入,则写者就处于饥饿

写者优先策略

  • 只要有写者就绪,写者应尽快执行写操作

  • 如写者持续不断就绪,则读者就处于饥饿

用管程实现写者优先




okToWrite.signal()只唤醒一个等待着的WW,但okToRead.broadcast()将所有等待着的WR唤醒。因为只允许一个写者,但读者可以有很多。

哲学家就餐问题

问题描述:


共享数据:

  • Bowl of rice (data set)
  • Semphore fork[5]  initializedto 1

take fork(i)  :P(fork[i])             put fork(i):V(fork[i])

方案1:


不正确,可能导致死锁,5个人都拿到了自己左边的叉子。

方案2

互斥访问正确,但每次只允许一人进餐

方案3


没有死锁,可有多人同时就餐

方案4:信号量解决

哲学家要么不拿,要么就拿两把叉子。那么哲学家就有三种状态:思考状态不用叉子、饥饿状态在等待左右叉子;吃饭状态正在使用叉子。

1.必须有一个数据结构,描述每个哲学家当前的状态

#define N 5           //哲学家个数
#define LEFT (i)       //第i个哲学家的左邻居
#define RIGHT (i+1)/N  //第i个哲学家的右邻居
#define EATTING 2     //进餐状态
#define HUNGRY  1      //饥饿状态
#define THINKING 0     //思考状态
int state[N];          //记录每个人的状态

2.该状态是一个临界资源,对它的访问应该互斥的进行

semaphore mutex;         //互斥信号量初始值为1

3.一个哲学家吃饱后,可能要唤醒邻居,存在同步关系

semaphore s[N];   //同步信号量 ,初值0

4.函数philosopher

void philosopher(i)
{
    think(i);
    take_forks(i);    //吃饭前先等待两只叉子
    eat();
    put_forks(i);     //放下叉子,查看左右邻居是否两只叉子都空闲,如果空闲提醒邻居拿起叉子
}
5.函数take_forks的定义:功能是要么拿到两把叉子,妖魔被阻塞起来。
void take_forks(i)
{
   P(mutex)
   state[i] = HUNGRY;               //代表当前哲学家正在等待叉子
   test_take_left_right_forks(i);   //尝试是否能拿到叉子

   V(mutex);
   P(s[i]);                        //如果拿不到叉子就阻塞
}

6.函数test_take_left_right_forks

void test_take_left_right_forks(i)
{
   if(state[i] == HUNGRY && state[LEFT] != EATTING && state[RIGTH] != EATTING)
   {
       state[i] = EATTING;    //用EATTING代表当前哲学家能拿到两只叉子
       V(s[i]);               //如果能够拿到两只叉子,唤醒当前线程
   }
}

7.函数put_forks的定义

void put_forks(i)
{
  P(mutex)
  state[i] = THINKING;  //代表当前不需要叉子
  test_take_left_right_forks(LEFT);
  test_take_left_right_forks(RIGHT);
  V(mutex);
}

8.eat应该拿到两把叉子,是临界区,不需要保护,但是think需要保护

void think(i)
{
   P(mutex);
   state[i] = THINKING;
   V(mutex);
}

方案4:信号量解决:管程解决

#define N 5
#define LEFT (i)
#define RIGHT (i+1)/N
#define EATTING 2
#define HUNGRY  1
#define THINKING 0
int state[N];
lock mutex;
Condition c[N];
void philosopher(i)
{
    think(i);
    take_forks(i);
    eat();
    put_forks(i);
}
void take_forks(i)
{
   lock.acqure();
   state[i] = HUNGRY;    //代表当前哲学家正在等待筷子,处于阻塞状态
   test_take_left_right_forks(i);   //尝试是否能拿到叉子
   while(state[i] != EATTING)
       c[i].wait(&lock);
   lock.release();

}
void test_take_left_right_forks(i)
{
   if(state[i] == HUNGRY && state[LEFT] != EATTING && state[RIGTH] != EATTING)
   {
       state[i] = EATTING;    //用EATTING代表当前哲学家能拿且会用叉子 
       condition[i].signal();      
   }

}

void put_forks(i)
{
  lock.acquier();
  state[i] = THINKING;  //代表当前不需要筷子
  test_take_left_right_forks(LEFT);
  test_take_left_right_forks(RIGHT);
  lock.release();
}

void think(i)
{
   lock.acquier();
   state[i] = THINKING;
   lock.release();
}



猜你喜欢

转载自blog.csdn.net/Alatebloomer/article/details/80047067