当前位置: 代码迷 >> 综合 >> 操作系统 - 进程篇4(进程同步-信号量与管程todo)
  详细解决方案

操作系统 - 进程篇4(进程同步-信号量与管程todo)

热度:81   发布时间:2023-12-16 19:59:46.0

一、背景

确保同步,需要高层次的编程抽象(锁),从底层硬件支持编译

二、信号量

1. 信号量抽象数据类型

 一个整型,两个原子操作:

  • P():sem减1,如果 sem<0,等待,否则继续
  • V():sem加1,如果sem<=0,唤醒一个等待的P

2. 概述

a. 特点

特点:信号量是整数,P()能阻塞,V()不会阻塞

信号量是被保护的变量:

  • 初始化完成后,唯一改变一个信号量的方法是通过P()和V()
  • 操作必须是原子的

信号量可以是公平的:

  • 如果V()被无限频繁调用,则没有线程被一直阻塞在P()
  • 信号量阻塞队列可以是FIFO的

b. 类型

  • 二进制信号量:可以是0或者1
  • 一般/计数信号量:可取任意非负值

两者互为表现(给定一个,可以实现另一个)

c. 作用场景

  • 互斥
  • 条件同步(调度约束 - 一个线程等待另一个线程的事件发生)

三、信号量使用

1. 用二进制信号量实现互斥

mutex=new Semaphore(1);// 加锁
mutex.P();// 操作
dosomething();// 解锁
mutex.V();

2. 使用二进制信号量实现调度约束

condition=new Semaphore(0);// 线程A
do something...
condition.P()   // 线程A等待B中 v()发出信号,以便进行下一步操作
do something...// 线程B
do something...
condition.V()   // 线程B 发出 V() 信号,唤醒线程A 等待
do somthing...

3. 例子

 

 伪代码实现:

/**
* 公共变量初始化
**/
mutex=new Semaphore(1);
fullBuffers=new Semaphore(0); // 用以标识buffer中物品数量
emptyBuffers=new Semaphore(n); // 用以标识buffer空间/**
* 生产者伪代码
**/
emptyBuffers.P();          // 申请空缓存区,若无空缓冲区,则等待进程
mutex.P();                 // 限制同一时刻只能有一个线程进入缓冲区
add c to the buffer;       // 操作并往缓冲区增加物品
mutex.V();                 // 解锁
fullBuffers.V();           // 增加缓冲区物品数/**
* 消费者代码
**/
fullBuffers.P();           // 判断缓冲区是否有物品,无物品则等待
mutex.P();                 // 限制同一时刻只能有一个线程进入缓冲区
sub c to the buffer        // 消费物品
mutex.V();                 // 解锁
emptyBuffers.V();          // 缓冲区空闲空间增加

注意:生产者与消费者程序中,V操作交换不会产生问题,但P操作顺序交换,可能导致死锁 

四、信号量实现

a. 实现方式

使用硬件原语: 禁用中断;原子指令(test-and-set)

例如:

b. 信号量问题

读写代码比较困难,容易出错,不能处理死锁问题 

五、管程

1. 概述

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

什么是管程:

  • 一个锁:指定临界区
  • 0或者多个条件变量:等待/通知信号量用于管理并发访问共享数据

一般方法:

  • 收集在对象/模块中的相关共享数据
  • 定义方法来访问共享数据

2. 流程

a. 管程组成

Lock:

  • Acquire:等待直到锁可用,然后抢占锁
  • Release:释放锁,如果有等待者,则唤醒

Condition Variable:

  • 允许等待状态进入临界区:允许处于等待(睡眠)的线程进入临界区
  • wait() 操作: 释放锁,睡眠
  • signal() 操作:唤醒等待者

b. 管程执行流程

管程原理流程:

  1. 进入管程需要一个队列,取得lock
  2. 进入管程后,可以执行管程维护的一系列函数
  3. 若线程资源得不到满足,需要挂在某个队列中,并释放lock
// 初始化
int numWating=0;
WaitQueue q;// 等待线程 wait(lock)
numWating++;          // 等待的线程数加一
add this thread to q; // 将线程加入到等待队列
release(lock);        // 释放线程持有的锁
schedule();           // 调度,需要加锁
require(lock);        // 获得调度的线程请求获得锁// 唤醒线程 signal()
if(numWating>0){Remove a thread t from q; // 将线程从等待队列中取出wakeup(t);                // 唤醒线程,需要加锁numWating--;              // 等待的线程数减1
}

c. 管程实现生产者/消费者问题

// 初始化
Lock lock;
int count=0;
Condition notFull,notEmpty;// 生产者
lock.Acquire();
while(count==n){           // buffer已经满了,生产者需要等待notFull.Wait(&lock);   // 线程于等待队列中等待,并释放锁
}
Add to buffer;             // 生产数据
count++;
notEmpty.Signal();         // 唤醒消费者
lock.Release();            // 释放锁// 消费者
lock.Acuqire();            // 获取锁
while(count==0){notEmpty.Wait(&lock);  // 等待审查者生产数据
}
Remove from buffer;        // 消费数据
count--;
notFull.Signal();          // 唤醒生产者
lock.Release();            // 释放锁

问题:

线程A发起signal() 后,是立即执行唤醒的线程,还是等线程A执行完毕再执行唤起的线程?若立即唤醒的线程,则管程中会同时出现两个线程

六、经典同步问题

1. 读者-写者问题信号量实现

a. 概述

b. 读者优先

// 初始化
CountMutex=new Semaphore(1);
WriteMutex=new Semaphore(1);
int count=0;// 写者
WriteMutex.P();         // 等待写锁
write;                  // 写入数据
WriteMutex.V(); // 读者
CountMutex.P();          // 计数器加锁
if(count==0){            // 如果不存在读者,则需要获取写锁,避免有写线程存在WriteMutex.P();      // 增加写锁
}
count++;                 // 读线程数量+1
CountMutex.V();read();                  // 读数据CountMutex.P();         
count--;  
if(count==0){WriteMutex.V();       // 计数器为0,释放写锁 
}
CountMutex.V(); 

 c. 写者优先

// 初始化
ReadCountMutex=new Semaphore(1);
WriteCountMutex=new Semaphore(1);
WriteMutex=new Semaphore(1);
ReadMutex=new Semaphore(1);
int writeCnt=0;
int readCnt=0;// 写者
WriteCountMutex.P();      // 获取计数器锁
if(writeCnt==0){            // 若计数器为0,则需要竞争读锁ReadMutex.P();   
}
writeCnt++;                // 写数量加1
WriteCountMutex.V();       // 请求写锁WriteMutex.P();            // 一次仅有一个线程可以写
write;
WriteMutex.V();             // 释放写锁WriteCountMutex.P();
writeCnt--;
if(writeCnt==0){ReadMutex.V();         // 释放读锁
}
WriteCountMutex.V();// 读者
ReadMutex.P();            // 申请锁,使得有写线程等待时,读线程需要排在写线程之后被唤醒
ReadCountMutex.P();       
if(readCnt==0){WriteMutex.P();       // 若不存在读线程,则需要等待写锁
}
readCnt++;                // 若在写线程之前,读线程之后,有读线程到达,不加锁,直接进入临界区
ReadCountMutex.V();         
ReadMutex.V();do_read();                 // 读数据ReadCountMutex.P();
readCnt--;
if(readCnt==0){WriteMutex.V();
}
ReadCountMutex.V();

2. 读者-写者问题管程实现(TODO)

思路:

3. 哲学家就餐问题(TODO)

  相关解决方案