ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2021-04-26 12:58:58  阅读:203  来源: 互联网

标签:同步 生产者 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是操作系统里睡觉和叫醒操作的原语。

  • 一个程序调用sleep后将进入休眠状态,其所占CPU将被释放。
  • 一个执行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操作将其叫醒);因此,二元信号量是从某种意义上说就是锁与睡觉、叫醒两种原语操作的合成。有了信号量,解决生产者和消费者的问题就可以这样:

  • 首先,对于item的操作不会出现数据竞争。(item操作均加锁mutex)
  • 其次,不会同时让消费者和生产者睡觉。(empty和full不同时为0)

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

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

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

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

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

8.7 管程

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

  • 管程是一个程序语言级别的构造,它的正确运行由编译器来保证。(这是计算机里面的一条原理:你不行的时候将事情交给别人)。
  • 管程就是将要同步的代码通过构造框来框起来,在任何时候只有一个线程活跃在管程内部;即将要保护的代码置于begin monitor和end monitor之间。在编译器编译的时候,发现begin monitor和end monitor就会对其进行同步操作的处理之后再转换成低级语言。
  • 管程使用了两种同步机制:锁(互斥)和条件变量(控制线程执行的顺序,即一个线程可以再上面等待的东西,另一个线程可以通过发送信号将在条件变量上的线程叫醒;类似于信号量却又不是信号量,因为没有up和down的操作)。
  • 管程的中心思想:运行一个在管程里面睡觉的线程,在进入管程前需要把进入管程的锁和条件变量释放(否则其他的线程将无法进入管程,因为这会造成死锁)。这里允许别的线程进入管程,因此线程可以在管程中睡觉。(这与其他机制不一样,一般来讲,线程在临界区呆的时间越长,别的线程等待的时间久越长,而这里正好相反)

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

@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 解决管程问题的方法:

  • HORSE管程:发送signal时同时释放锁,让被叫醒者获得锁(在signal之后运行的线程将是被叫醒的线程),叫醒者本身只能在被叫醒者运行完毕/其他原因释放锁后才能运行。
  • MESA管程:不能在管程设计时就确定了,因为这样做十分不灵活,对操作系统而言下一步执行哪个操作应该由它决定(这样就可以利用操作系统的机制来竞争这把锁)。

8.8 消息传递

管程机制的问题:

  • 对编译器的依赖,而实际上多道胡编译器也没有实现管程机制。
  • 只能在单个计算机上面运行,严重限制了其使用。

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

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

消息传递的问题:

  • 消息丢失:在一台计算机上基本不会,但是在网络上则很有可能,因为网络的不可靠性(可以通过网络协议如TCP/IP,可以将数据传输的可靠性提高,但不是100%)。
  • 身份认证:怎样知道消息是从哪里发出来的(可以通过网络协议以及数字签名和加密认证等方式来解决)。
  • 效率低下:往返发送系统消息存在系统消耗,同时数据传输也有延迟(尤其是在网络比较慢的时候)。

8.9 栅栏(barrier)

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

栅栏的参考模型如下:

 

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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有