光有lock可不够,信号量与管程原理

实际场景中,光有锁和互斥是不够的,还需要一些复杂的同步机制,比如读写锁(多个读操作可以同时进行),比如生产者消费者模型(生产者消费者需要你等我我等你,你唤醒我,我唤醒你,这样的同步协调),在这种情况下,我们只使用锁机制就不够了,需要更高层的同步互斥的语义来实现

两个名词解释,有助于接下来的理解:

临界区(critical section):一段需要读写共享资源的代码

互斥(Mutual exclusion):一个线程进入临界区后,其他线程就进入不了

好了,接下来进入今天的正题,高级同步互斥原语信号量以及管程的原理介绍

信号量:

信号量允许小于等于信号量值的线程数同时进入临界区,可以把这个值设置为等于资源数,资源有1个,同时只能有一个线程使用,资源有多个,就有多个线程可以同时使用

模型:包含一个整型数字(sem),两个原子操作,P():获取信号量,sem减1,如果sem<0,说明信号量不够了,阻塞,否则继续;V():释放信号量,sem加1,如果释放后sem<=0,说明有线程等待在P操作上,唤醒一个等待的线程(也可以全部唤醒),伪代码如下:

class Semaphore{

       int sem;

       WaitQueue waitQ;

}

Semaphore::P(){

       sem--;

       if(sem<0){

              Add this thread to waitQ;//将当前线程加入等待队列

            .....睡眠中//等待队列中的线程不会被操作系统调起,直到被唤醒

       }

}

Semaphore::V(){

       sem++;

       if(sem<=0){//加1后还<=0表示之前是<0的,等待队列中肯定有元素

              remove a thread t from waitQ;//从等待队列中取出一个线程

              notify(t);//唤醒这个线程,将线程状态置为ready,可被操作系统调起

       }

}

如果信号量>1,可以实现多个线程同时进入临界区的效果,比如多个线程抢5张票,允许5个线程同时抢,称为计数信号量

如果信号量=1,则可以等同于互斥效果,A线程调用P()后,B线程再调用P()就会进入阻塞,称为互斥信号量

如果信号量=0,则可以实现同步协调的效果,A线程调用P()后被阻塞,B线程调用V()后唤醒A线程,实现A等B的效果,称为同步信号量

管程: 

管程的抽象程度比信号量更高,对于开发者来说,一个工具抽象程度越高,理解以及使用起来就更简单。信号量一开始就被设计用来解决操作系统层面的同步互斥问题,而管程则是诞生于开发语言的level,这些语言通过管程的设计,来简化开发者编写同步互斥代码的难度

管程,又叫monitor(监视器,监管的含义),包含了一系列的共享变量以及对这些共享变量的操作函数的组合

具体的设计中,管程组成:

       1个互斥锁,用于指定临界区保证互斥,保证同时只能有一个线程访问管程所管理的函数

       0个或多个条件变量,因为会存在一些共享资源和变量,在访问过程中会有些条件得不到满足(比如消费者发现没东西可消费了),就需要把那些得不到满足的线程挂起,就挂起在条件变量上(实际上是存在该条件变量对应的waitQueue中),当某个条件变成满足后(比如原本没产品了,生产者又生产了一个),应该唤醒之前挂起在这个条件变量上的线程。有多少条件,就应该设置多少条件变量

       有点需要注意的是,线程进入临界区,需要拿到互斥锁,当线程执行过程中发现某个条件变量不满足需要挂起自己的时候,会同时释放掉互斥锁

互斥锁以及条件变量的组合,就可以实现管程,下面还是看看管程的伪代码:

       互斥锁Lock的原理不用多说,可以参考前面的章节"lock是如何实现的",我们着重看下条件变量的实现:

       Class Condition{

              int numWaiting=0;//等待的线程数量

              WaitQueue waitQ;//每个条件变量对应一个等待队列,存储需要等待这个条件满足的线程信息

       }

       Condition::Wait(lock){

              numWaiting++;//等待的线程数增1

              add this thread t to waitQ;//线程加到等待队列中

              release(lock);//wait操作一定是获得锁的情况下执行的,这里要释放掉已经拿到的锁,让其他线程可以执行

              schedule();//这一步可以理解为留一定的时间给其他线程执行

              acquire(lock);//为啥这里还要再获取锁呢?请你想一想,然后带着问题,我将在生产者消费者模型一文为你解惑

       }

       Condition::Signal(){

              if(numWaiting>0){//如果没有线程在等待,就不会执行

                     remove a thread t from waitQ;//从等待队列中移出一个线程,FIFO方式

                     wakeup(t);//唤醒这个线程,让其变成ready状态,可以竞争条件变量

                     numWaiting--;

              }

       }

信号量和管程的实现原理已经说完了,在实际应用中,又是如何使用它们实现复杂的同步互斥需求的呢?

下面两章,我将说明如何使用信号量、管程实现本文开头说的读写锁,生产者消费者模型

PS:如果有不足之处,欢迎指出;如果解决了你的疑惑,就点个赞吧o(* ̄︶ ̄*)o

猜你喜欢

转载自blog.csdn.net/wb_snail/article/details/105391211