信号量、管程

信号量

1、OS 提供的一种协调共享资源访问的方法

2、抽象数据结构,一个整型 int(sem),可进行两个原子操作

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

(2)V():sem 加 1,如果 sem <= 0,唤醒挂在信号量上的进程,可以是一个,可以是多个

3、特性

(1)信号量是整数

(2)信号量是被保护变量

(3)初始化完成后,只能通过 P()、V() 操作修改

(4)P()、V() 必须为原子操作

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

4、类型 

(1)二进制信号量:约等于锁,取值 0 或 1

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

5、使用

(1)互斥

(2)条件同步(调度约束:一个线程等待另一个线程的事情发生)

6、二进制信号量实现锁的互斥

(1)初始值设置为 1

(2)进入临界区之前,执行 P()

(3)退出临界区之后,执行 V()

mutex= new semaphore(0)//设置一个信号量初值为0
mutex->P();
Critical section
mutex->V();

7、二进制信号量实现锁的条件同步

(1)初始值设置为 0

(2)一个线程执行 P(),等待另一个线程执行 V() 发出信号

8、事项

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

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

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

(4)P()、V()操作不能次序错误、重复或遗漏

 

有限缓冲问题

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

2、缓冲区空时,消费者必须等待生产者(条件同步)

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

4、用信号量描述在这个问题当中的约束

(1)二进制信号量 mutex:表示 I/O 是否被占用,初始化为 1

(2)资源信号量 fullBuffers:表示当前占满的缓冲区数量,用于限制读出,初始化为 0

(3)资源信号量 emptyBuffers:表示当前为空的缓冲区数量,用于限制写入

class BoundedBuffer{
	mutex = new Semaphore(1);
	fullBuffers = new Semaphore(0);
	emptyBuffers = new Semaphore(n);
}

BoundedBuffer::Deposit(c){
	emptyBuffers->P();//条件同步
	mutex->P();    //互斥访问
	Add c to buffer; //核心操作
	mutex->V();    //互斥访问
	fullBuffers->V();//条件同步
}

BoundedBuffer::Remove(c){
	fullBuffers->P();//条件同步
	mutex->P();    //互斥访问
	Add c to buffer; //核心操作
	mutex->V();    //互斥访问
	emptyBuffers->V();//条件同步
}

 

管程

1、目的:分离互序和条件同步的关注

2、包含了一系列的共享变量,以及针对这些变量的操作的函数的组合 / 模块

(1)一个锁:指定临界区,确保互斥性

(2)0 或者多个条件变量:根据条件的个数决定,等待 / 通知信号量,用于管理并发访问共享数据

3、一般方法

(1)收集在对象 / 模块中的相关共享数据

(2)定义方法来访问共享数据

4、锁

(1)Lock::Acquire():等待直到锁可用,然后抢占锁

(2)Lock::Release():释放锁,唤醒等待者

5、条件变量

(1)管程内的等待机制:进入管程的线程因资源被占用而进入等待状态,每个条件变量表示一种等待原因,对应一个等待队列

(2)允许处于等待(睡眠)的线程进入临界区,某个时刻原子释放锁进入睡眠

(3)Wait() 操作:睡眠,释放管程的互斥访问,

(4)Signal() 操作:唤醒自己队列中的一个线程,如果等待队列为空,则等同空操作

(5)Broadcast() 操作:唤醒自己队列中的多个线程,如果等待队列为空,则等同空操作

(6)实现

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

Condition::Wait(lock) {
    numWaiting++;
    Add this thread t to q;
    release(lock);
    schedule();//need mutex
    require(lock);
}

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

(7)解决有限缓冲问题

classBoundedBuffer {
    Lock lock;
    int count = 0;
    Condition notFull, notEmpty;
}

BoundedBuffer::Deposit(c) {
    lock->Acquire();
    while(count == n) {
        notFull.Wait(&lock);
    }
    Add c to the buffer;
    count++;
    lock->Release();
}

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

 6、管程中条件变量的不同的释放处理方式

(1)Hansen 管程:正占用管程处于执行状态的线程优先执行,执行 signal()、release() 之后,再交出 CPU 控制权,给被唤醒的线程

(2)(例)流程:T1 进入等待 -> T2 进入管程 -> T2 退出管程 -> T1 恢复管程执行

(3)特点:先唤醒,后交接,可能存在多个线程被唤醒,对锁、CPU 进行抢占,切换次数较少,实际使用效率更高,容易实现

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

(4)Hoare 管程:内部的线程优先执行,即一旦发出 signal() 操作,就睡眠,交出 CPU 控制权,给被唤醒的线程,

(5)(例)T1 进入等待 -> T2 进入管程 -> T2 进入等待 -> T1 恢复管程执行 -> T1 结束 -> T2 恢复管程执行

(6)特点:先交接,后唤醒,正确性更容易说明,更适合理论分析,实现复杂

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

 

读者-写者问题

1、动机:共享数据的访问

2、两种类型使用者,多个并发进程的数据集共享

(1)读者:不需要修改数据,只读数据集,不执行任何更新

(2)写者:读取和修改数据

3、问题的约束

(1)允许同一时间有多个读者,但在任何时候只有一个写者

(2)当没有写者时,读者才能访问数据

(3)当没有读者和写者时,写者才能访问数据

(4)在任何时候只能有一个线程可以操作共享变量

4、共享数据

(1)数据集

(2)信号量 CountMutex 初始化为 1

(3)信号量 WriteMutex(写者数量) 初始化为 1

(4)整数 Rcount(读者数量) 初始化为 0

5、策略

(1)基于读者优先策略的方法:只要有一个读者处于活动状态,后来的读者都会被接纳,如果读者源源不断地出现的话,那么写者就始终处于阻塞状态

Database::Write() {
    sem wait(WriteMutex);
    write;
    sem post(WriteMutex);
}

Database::Read() {
    sem wait(CountMutex);
    if (Rcount == 0) {
        sem wait (WriteMutex);
        ++Rcount;
    }
    sem post(CountMutex);
    read;
    sem wait(CountMutex);
    --Rcount;
    if (Rcount==0) {
        sem post(WriteMutex);
    }
    sem post(CountMutex);
}

(2)基于写者优先策略的方法:一旦写者就绪,那么写者会尽可能快地执行写操作,如果写者源源不断地出现的话,那么读者就始终处于阻塞状态

MonitorStateVariables {
    AR = 0;//#of active readers
    AW = 0;//#of active writers
    WR = 0;//#of waiting readers
    WW = 0;//#of waiting writers
    Condition okToRead;
    Condition okToWrite;
    Lock lock;
}

Public Database::Read() {
    //Wait until no writers;
    StartRead();
    read database;
    //check out - wake up waiting writers;
    DoneRead();
} 

Private Database::StartRead() {
    lock.Acquire();
    while ((AW + WW) > 0) {
        WR++;
        okToRead.wait(&lock);
        WR--;
    }
    AR++;
    lock.Release();
} 

Private Database::DoneRead() {
    lock.Acquire();
    AR--;
    if (AR == 0 && WW > 0) {
        okToWrite.signal();
    }
    lock.Release();
}

Public Database::Write() {
    //Wait until no readers / writers;
    StartWrite();
    write database;
    //check out - wake up waiting readers / writers;
    DoneWrite();
}

Private Database::StartWrite() {
    lock.Acquire();
    while ((AW + AR) > O) {
        WW++;
        okToWrite.wait(&lock);
        WW--;
    }
    AW++;
    lock.Release();
}

Private Database::DoneWrite() {
    lock.Acquire();
    AW--;
    if (WW > 0) {
        okToWrite.signal();
    } else if(WR > 0) {
        okToRead.broadcast();
    }
    lock.ReleaseO;
}

 

哲学家就餐问题

1、问题描述

(1)假设有五位哲学家围坐在一张圆形餐桌旁

(2)做以下两件事情之一:吃饭,或思考,吃东西时,停止思考,思考时,停止吃东西

(3)每两个哲学家之间有一只餐叉,哲学家必须用两只餐叉吃东西,且只能使用自己左、右手边的那两只餐叉

2、原则:不能浪费 CPU 时间,进程问相互通信

3、思路

(1)思考

(2)进入饥饿状态

(3)如果左邻居或右邻居正在进餐,进程进入阻塞态,否则转(4)

(4)拿起两把叉子

(5)吃面条

(6)放下左边的叉子,看看左邻居现在是否为饥饿状态,若两把叉子都在,则唤醒之

(7)放下右边的叉子,看看右邻居现在是否为饥饿状态,若两把叉子都在,则唤醒之

(8)转入(1)

4、数据结构描述每个哲学家的当前状态

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

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

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

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

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

7、定义函数 philosopher

void philosopher(int i)//i的取值:0到N-1
{
    while(true)//封闭式循环
    {
        think();//思考
        take_forks(i);//拿到两把叉子或被阻塞
        eat();//进食
        put_forks(i);//把两把叉子放回原处
    }
}

8、定义函数 takeforks:要么拿到两把叉子,要么被阻塞

void take_forks(int i)//i的取值:0到N-1
{
    P(mutex);//进入临界区
    state[i] = HUNGRY;//饥饿状态
    test_take_left_right_forks(i);//试图拿两把叉子
    V(mutex);//退出临界区
    P(s[i]);//没有叉子便阻塞
}

9、定义函数 put_forks:把两把叉子放回原处,并在需要的时,去唤醒左邻右舍

void put_forks(int i)//i的取值:0到N-1
{
    P(mutex);//进入临界区
    state[i] = THINKING;//交出两把叉子
    test_take_left_right_forks(LEFT);//左邻居是否饥饿
    test_take_left_right_forks(RIGHT);//右邻居是否饥饿
    V(mutex);//退出临界区
}

10、定义函数 test_take_left_right_forks

void test_take_left_right_forks(int i);//i:0到N-1
{
    if (state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING)
    {
        state[i] = EATING;//取得两把叉子
        V(s[i]);//通知第i人可以吃饭了
    }
}
posted @   半条咸鱼  阅读(165)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示