• 操作系统的信号量详解


    信号量的定义

    信号量,也叫信号灯,是荷兰的计算机科学家发明,用来解决并发中的互斥和同步的问题,一般操作系统都实现了信号量,并提供了简单的调用方式。

    信号量控制的是线程,即最小调度单元。

    信号量有三个操作:

    • 初始化一个信号量(非负整数),
    • P操作,也叫 Wait,获取资源。
    • V操作,也叫 Signal,释放资源。

    信号量的实现过程

    struct semaphore {
    	int count;
    	queueType queue;
    }
    
    void acquire(semaphore s) {
    	s.count--;
    	if (s.count < 0) {
    		put this process in s.queue;
    		block this process;
    	}
    }
    
    void release(semaphore s) {
    	s.count++;
    	if (s.count <= 0) {
    		remove a process P from s.queue;
    		put process P in ready list;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    过程

    • 初始化:首先我们要初始化一个 semaphore 对象,设置 count 的值。
    • P操作:调用 acquire 获取资源,先将 count减1,如果小于0,说明资源不够了,这个线程会被加入到此信号量对应的等待队列中,同时此线程被阻塞,让出CPU。
    • V操作:调用 release 释放资源,先将 count加1,如果小于等于0 ,说明队列中还有线程待唤起,此时会以FIFO的方式拿出一个线程,并放到就绪队列,此线程就被唤醒了。V操作不会阻塞。

    P和V都是由操作系统和硬件来保证的原子操作,且不会被中断。

    最开始只有二元信号量(二进制信号量),即0和1,是为了解决互斥的问题。后来扩展到多值信号量,或计数信号量,可以用来解决同步的问题。

    互斥的实现

    从上面的代码中,我们可以得出,如果将count初始值设置为1,那么只有一个线程可以得到资源,且只有等它释放了资源,其他线程才能得到资源,这就实现了互斥的效果。

    同步的实现

    如果将count初始值设置为0,并伴有如下过程。

    1. 线程A执行P操作,此时 s == -1,会被阻塞。
    2. 线程B执行V操作,此时 s == 0,线程A被唤醒。
    3. 可见线程A要等到线程B执行完毕才会被执行,这就是同步。

    计数信号量

    当然,count可以被设置为更大的值,比如100,于是就有了带有缓冲区的生产者消费者模型。

    生产者 --> buffer --> 消费者

    1. 其中生产者和消费者都可以是多个。
    2. 在任何时刻只有一个生产者或消费者可以访问缓冲区。
    3. 当缓冲区满了之后,所有的生产者都要等待。
    4. 当缓冲区为空时,所有的消费者都要等待。

    针对这些需求,我们可以设置三个信号量。

    class BoundedBuffer {
    	// 互斥锁,解决问题2
    	mutex = new semaphore(1); 
    	
    	// 计数器,初始值为0,用以判断缓冲区是否为空,为0就是空了
    	consumeMutex = new semaphore(0); 
    	
    	// 计数器,初始值为n,用以判断缓冲区是否满了,为0就是满了
    	produceMutex = new semaphore(n); 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    然后使用伪代码来表示这个过程。

    function Produce(t) {
    	produceMutex->P();
    	
    	mutex->P();
    	add t task to buffer
    	mutex->V();
    	
    	consumeMutex->V();
    }
    
    function Consume() {
    	consumeMutex->P();
    
    	mutex->P();
    	remove t task to buffer
    	mutex->V();
    	
    	produceMutex->V();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
  • 相关阅读:
    面试面经|Java多线程面试题
    拥抱 Spring 全新 OAuth 解决方案
    【微服务】Day14(续 秒杀业务准备、消息队列)
    C++ Reference: Standard C++ Library reference: C Library: cwchar: wcslen
    轴及其加工工艺
    ARM汇编
    【Android性能】【流畅度】概念初识
    搞定蓝牙——第四章(GATT协议)
    k8s 污点和容忍
    常用类以及接口
  • 原文地址:https://blog.csdn.net/raoxiaoya/article/details/125555368