操作系统笔记--信号量、管程与同步

1--信号量

1-1--基本知识

        信号量可用于实现线程间的互斥与同步,其抽象数据类型包括:一个整型数据(Sem)和两个原子操作(P() 和 V());

        P() 操作:将 Sem 减1,如果 Sem < 0,则执行 P() 操作的线程会等待,否则继续执行;

        V() 操作:将 Sem 加1,如果 Sem <= 0,则执行 V() 操作的线程会唤醒一个等待的线程;

信号量基本概念:

       ① 信号量是一个整数,也是一个被保护的变量,只能通过 P() 和 V() 原子操作来改变信号量的值;

       ② P() 操作会阻塞,V() 操作不会阻塞;

       ③ 信号量是公平的,V() 操作唤醒线程也是公平的,一般采取 FIFO(先进先出)算法来唤醒等待队列中的线程; 

        ④ 信号量有两种类型:第一是二进制信号量,用0和1表示;第二是计数信号量,可用任何非负值表示;

        ⑤ 信号量的用途:实现线程间的互斥和条件同步;

// 二进制实现线程间的互斥

mutex = new Semaphore(1); // 初始化为1

mutex->P(); // P操作,减1,判断是等待还是继续执行
...
Critical Section; // 临界区
...
mutex->V(); // V操作,加1,是否唤醒一个等待的线程
// 二进制信号量实现同步调度

condition = new Semaphore(0); // 初始化为0

// 线程A
...
condition->P(); // 由于condition初始值为0,执行P()操作变成负数,线程A处于等待状态
    ...

// 线程B
...
condition->V(); // 执行V()操作,唤醒处于等待状态的线程A
    ...

// P操作使线程A等待,直到线程B执行V操作来唤醒调度线程A

1-2--基本实例

使用信号量实现线程间同步互斥的实例:有界缓冲区的生产者和消费者问题

        ① 一个或多个生产者产生数据并将数据存放在一个缓冲区内;

        ② 单个消费者每次从缓冲区中取出数据;

        ③ 在任何一个时间里只有一个生产者或消费者可以访问缓冲区;

上述实例抽象三个出三个要求:

        ① 在任何一个时间只能有一个线程操作缓冲区(互斥);

        ② 当缓冲区为空时,消费者必须等待生产者(调度/同步约束);

        ③ 当缓冲区为满时,生产者必须等待消费者(调度/同步互斥);

Class BoundedBuffer{
    mutex = new Semaphore(1); // 互斥,初始为1
    fullBuffers = new Semaphore(0); // 同步,初始为0,可理解为buffer当前存量
    emptyBuffers = new Semaphore(n); // 同步,初始为n,可理解为buffer剩余容量
}

// 生产者
BoundedBuffer::Deposit(c){
    emptyBuffer->P(); // 剩余容量减1,当为负值时,说明buffer已满,阻塞所有生产线程,直到生产线程唤醒
    mutex->P(); // 互斥锁,只允许一个线程进行临界区
    Add c to the buffer;
    mutex->V(); // 退出临界区,解锁
    fullBuffer->V(); // 当前存量加1
}

// 消费者
BoundedBuffer::Remove(c){
    fullBuffers->P(); // 当前存量假1,当为负值时,说明buffer已空,阻塞所有消费线程,直到生产线程唤醒
    mutex->P(); // 互斥锁,只允许一个线程进行临界区
    Remove c from buffer;
    mutex->V(); //退出临界区,解锁
    emptyBuffers->V(); // 剩余容量加1
}

1-3--信号量实现

classSemaphore{
    int sem;
    WaitQueue q;
}

Semaphore::P(){
    sem--;
    if(sem < 0){
        Add this thread t to q; // 将线程t置于等待队列q
        block(p); // 睡眠
    }
}

Semaphore::V(){
    sem++;
    if(sem <= 0){
        Remove a thread t from q; // 将线程t从等待队列q中移除
        wakeup(t); // 唤醒等待队列q中的线程t
    }
}

2--管程

2-1--基本知识

        信号量机制存在编写程序困难、容易出错等问题;为了更好地实现同步互斥,引入一个抽象的概念称为管程;

        管程可抽象为由一个锁和 0 个或多个条件变量构成的结构,锁用于指定临界区(每次只允许一个线程进入管程管理的区域),条件变量用于控制线程的等待和唤醒;

Lock:

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

        Lock::Release():释放锁,唤醒等待的线程;

Condition Variable:

        Wait():释放锁,线程睡眠

        Signal():唤醒等待的线程

Class Condition{
    int numWaiting = 0; // 等待的线程数
    WaitQueue Q; // 等待队列
}

Condition::Wait(lock){
    numWaiting++; // 等待的线程数加1
    Add this thread t to q; // 将线程t置于等待队列中
    release(lock); // 释放锁
    schedule(); // 选择下一个线程执行
    require(lock); // 获取锁
}

// 等待操作先释放锁的原因:
// 管程有很多入口,但每次只能开放其中一个入口,只能让一个进程或线程进入管程管理的区域
// 当线程使用wait()条件变量时,因为自身处在等待状态,这时需要释放管程的使用权,也就是释放管程的入口来让其他线程进入,因此要释放锁

Condition::Signal(){
    if(numWaiting > 0){ // 表明当前有线程正在等待
        Remove a thread t from q; // 从等待队列中移除一个等待的线程t
        wakeup(t); // 唤醒线程t
        numWaiting--; // 等待线程数减1
    }
}

2-2--基本实例

        利用管程实现生产者和消费者的问题:

// 初始化
classBoundedBuffer{
    Lock lock; // 锁
    int count = 0; // 当前容量
    // notFull条件变量管理生产线程
    // notEmpty条件变量管理消费线程
    Condition notFull, notEmpty;
}

// 生产者
BoundedBuffer::Deposit(c){
    lock->Acquire(); // 实现互斥,因为管程每次只允许一个线程或进程通过
    while(count == n) // 容量已满,线程循环等待
        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();
}

3--经典同步问题

猜你喜欢

转载自blog.csdn.net/weixin_43863869/article/details/130724356