其他分享
首页 > 其他分享> > 操作系统哲学原理(08)线程原理-线程同步

操作系统哲学原理(08)线程原理-线程同步

作者:互联网

说明:该系类文章主要是从哲学视角看 操作系统 这门学科。同时也是 博主阅读《操作系统之哲学原理》的笔记总结。因为博主 这些年主要是以研究安卓系统和 嵌入式Linux为主,因此这个系类文章也是这两个领域不可或缺的基石之一,尤其是对操作系统感兴趣的伙伴可特别关注。


8 线程同步

8.1 为什么要同步

8.2 线程同步的目的

8.3 锁的进化:金鱼生存

金鱼生存问题是一个演示线程同步手段的好例子。金鱼的特点:没有饱的感觉,喂多少就吃多少。

假设佐伊和尤尔共同养了一条金鱼,为把金鱼养好,即不让鱼胀死,也不饿死,做出如下约定:

在没有同步的情况下,佐伊和尤尔的执行顺序如下;

但是由于线程可以任意穿插,则执行结果可能如图所示:

很明显,这样的话鱼会胀死的。这里就涉及 新概念:竞争和临界区。

8.3.1 变形虫阶段

要防止鱼胀死,就要防止竞争;即防止两个/多个线程同时进入临界区。因此要协调。协调的目的就是任何时候只有一个人在临界区内,这称为互斥;即一次只有一个人使用共享资源。

正确互斥需要4个条件(只要有一个条件不满足,互斥的设计就是不正确的):

  1. 不能有2个进程同时在临界区里面。
  2. 进程要能够在任何数量和速度的CPU上正确执行。
  3. 在互斥区外不能阻止另一个进程的运行。
  4. 进程不能无限制的等待进入临界区。

通过交谈,佐伊和尤尔商定在喂鱼之前留字条,这是第一种同步机制,如图所示:

此方案有所改善,即降低了鱼胀死的概率,但没有完全解决问题(佐伊和尤尔交叉执行上述程序,还会造成鱼胀死的结局)。如下图所示:

8.3.2 鱼阶段

查看上一阶段解决不了问题的原因:没有先检查有没有字条后留字条,因此造成了空当。即检查字条和留字条之间有空隙。解决方法:先留字条后再检查又没有字条。改进的留字条的方法如图所示:

这样两个人就不会同时进入临界区了。因此鱼不会因为两个人都喂而胀死。但是如果程序穿插执行,效果如图所示:

那么鱼有被饿死的可能,这是一种进步,但是并没有完全解决问题。

8.3.3 猴阶段

查看上一阶段解决不了问题的原因:除了互斥之外,还要确保有一个人进入临界区来喂鱼。解决方法:让某个人等着,知道确认有人喂了鱼才离开,不要一见到字条就离开。改进的循环等待模式如下:

这个方案鱼既不会饿死、也不会胀死,但是程序本身不对称。

8.3.4 锁

查看上一阶段解决不了问题的原因:程序不对称(程序的编写会很困难,同时增加了证明的难度);时间、资源的浪费(循环等待,这可能会造成CPU调度的优先级倒挂)。解决方法的分析:循环等待不能去掉(如果这样那么就回到第二种同步机制上了);两者都对称、美观(使鱼饿死成为可能)。

解决方法:这个解决问题的思考方向有问题,我们需要换一种思路来思考这个问题。对之前的每个方案进行修改:将检查字条和留字条合并成一个原子操作,即提高抽象的层次,将控制层面上升到对一组指令的控制。于是锁的概念出现了(锁的原始模型:只能有一个人在教室里,只要进去就上锁,出来就闭锁)。加锁后的程序如图所示:

这样,问题就可以解决了。锁的基本操作:闭锁和开锁

闭锁操作的步骤(2个步骤是一个原子操作):

  1. 等待锁达到打开状态
  2. 获得锁并且锁上

开锁操作的步骤:

  1. 打开锁

锁的特性规则:

正确使用锁以后程序就可以正常运行,同时变得容易了。问题是解决了,但是能不能更好地解决呢,即缩短别人持有锁时自己等待的时间。仔细分析发现,喂鱼并不需要在持有锁的时候进行。只要在检查字条和留字条的地方加锁就可以。执行过程如图所示:

等待时间因此而大幅度缩短了,但是等待终究是需要时间的,下面需要考虑的就是有没有不需要等待的方法。

8.4 睡觉与叫醒:生产者与消费者问题

睡觉与叫醒:如果锁被对方持有,则不需要等待锁变为打开状态,而是睡觉去;锁打开以后再把你叫醒。消费者和生产者的问题是一个演示这种机制的一个较好的例子。

模型静态说明:         

模型动态说明:

用计算机模拟生产者和消费者:一个进程代表生产者;一个进程代表消费者;一片内存缓冲区就代表我们的商店。生产者生产物品从一端放入缓冲区;消费者从另一端获取物品,如图所示:

sleep和wakeup是操作系统里睡觉和叫醒操作的原语。

消费者/生产者的同步程序如图所示:

程序的逻辑没有问题。但是这个count有问题,因为变量没有被保护,可能存在数据竞争的问题,即生产者和消费者同时对该数据进行修改。这个问题可以通过锁的方案来解决,因为时间很短,可以接受。问题的关键是有可能造成死锁,即消费者和生产者进程均无法推进(存在信号丢失问题:即消费者正准备睡觉,但是生产者已经发出信号,则此信号无效,因为消费者没有处于睡觉的状态)。解决的方法就是不能让两者同时睡觉。而这本质的原因就是信号丢失,只要用某种方式方信号累积起来而不是丢掉,那么问题就解决了。于是新的机制出现了:能够将信号累积起来的操作在操作系统里叫做信号量。

8.5 信号量

semaphore(信号量)不只是同步的原语,还是通信原语。同时还可以作为锁来使用。

@1 同步原语:信号量实际上就是一个计数器,取值为当前累积的信号数量,支持两个操作,up和down(也称为p、v操作)

down操作:

  1.      判断信号量的取值是否>=1。
  2.      如果是,则将信号值-1,继续往下执行。
  3.      否则在该信号上等待。

up操作:

  1.      将信号量的值加+1。
  2.      线程继续向下执行。

注意:虽然down和up是多个步骤,但是是一组原子操作。

@2 锁原语:如果将信号量的取值限制为0和1两种情况,则我们获得的就是一把锁,也即二元信号量,操作如下:

二元信号量down操作:

  1. 等待信号量取值变为1;
  2. 将信号量的值设为0;
  3. 继续执行。

二元信号量up操作:

  1. 将信号量的值设为1;
  2. 叫醒该信号上面等待的第1个线程;
  3. 线程继续执行;

由于二元信号量的取值只有0和1,因此可以防止任何两个程序同时进入临界区。具备锁的功能,与锁很相似(down是获得锁、up是释放锁),却比锁灵活(在信号量上的线程不是等待,而是睡觉等待另一个线程执行up操作将其叫醒);因此,二元信号量是从某种意义上说就是锁与睡觉、叫醒两种原语操作的合成。有了信号量,解决生产者和消费者的问题就可以这样:

其中full和empty对应的是一个缓冲区,但是对于消费者和生产者,它们等待的信号是不同的,因此它们需要睡在不同的信号上(一个满,一个空)。

8.6 锁、睡觉和叫醒、信号量

操作系统的原语并不是没有联系,而是一环扣一环的,具有严密的逻辑性。

使用信号量的缺陷:当少于3个信号量时,顺序很容易掌握,但是对于多个信号量,down与up的顺序就不那么容易掌握了,而此时写程序也就变得很复杂了。(如果一个程序的信号繁多,死锁或者效率低下几乎是肯定的)

要想改变这种情况,就需要操作系统自己管理这些东西,这个方法就是管程。

8.7 管程

@1 信号量存在程序编写困难和执行效率低下的问题,那么交给操作系统做这个就可以了,这个新的东西就是管程(monitor,也叫监视器,监视的就是同步的操作)。

实现锁的释放和睡觉这两件事情必须是一个原子操作(因为如果有空档,将会造成有两个线程活跃在管程内)。

@2 利用管程实现生产者和消费者的同步:

@@2.1 生产者与消费者的管程内部部分如下:

生产者和消费者对缓冲区的访问都是在管程里面;因此,对线程的访问,对count计数器的修改都是互斥的。

@@2.2 生产者与消费者的管程外部部分如下:

生产者生产出商品,并调用insert函数将商品放入缓冲区中;消费者消费商品,调用remove函数将商品从缓冲区中取走。

@3 整个管程中没有加锁(编译器自动检测并加锁)。其中

wait以原子操作实现3个步骤:

  1. 释放锁;将本线程挂在条件变量x的等待队列上;
  2. 睡觉;
  3. 等待被叫醒;

signal实现的操作: 将等在条件变量上的第一个线程叫醒。(在叫醒方面还提供了一种机制,广播broadcast,在调用wait、signal、broadcast时该线程必须持有与管程相连的锁)整个过程与之前的sleep、wakeup操作类似,但是不同的是:管程不会发生死锁(sleep与wakeup方案中将要睡觉和睡觉这2个操作中存在空档)

注意:如果一个线程释放等待信号线程的signal,则此时有两个线程活跃在管程内部(即signal不是线程最后的操作,那么后面的操作就和新的线程一起在管程里面了),这违反了管程的约定。为了防止这种问题发生,管程机制特别规定:signal语句是一个线程在管程里面的最后一个操作(因为这样即使理论上有两个线程活跃于管程内,但是实际上只有一个线程活跃,因为一个线程的下一步操作已经在管程之外,从而维持我们关于管程的约定)。

@4 解决管程问题的方法:

8.8 消息传递

管程机制的问题:

于是想在多计算机环境下进行同步,那就需要其他的机制了。这种机制就是消息传递。消息传递是通过同步双方经过相互接收、发送消息来实现(send与receive操作,均为系统调用,既可以阻塞也可以非阻塞)。用消息传递机制实现生产者与消费者之间的同步问题:

对于该问题,需要send和receive均为阻塞操作,即执行receive操作需要收到消息后返回,否则将挂起。这种机制对于生产者与消费者之间的同步问题:既不会死锁,也不会繁忙等待,而且没有区域限制(可以跨计算机同步)。因此当前使用较为普遍。

消息传递的问题:

8.9 栅栏(barrier)

通信原语barrier:到达栅栏的线程必须停下来等待,直到障碍解除才能往前推进(主要用来对一组线程进行同步,有些时候,需要几个线程汇合在一起,协同完成任务)。

栅栏的参考模型如下:

 

标签:同步,生产者,08,哲学原理,信号量,线程,操作,管程
来源: https://blog.csdn.net/vviccc/article/details/116149464