说明:该系类文章更多的是从从哲学视角看 操作系统 这门学科。同时也是 操作系统的学习笔记总结。因为博主 这些年主要是以研究安卓系统和 嵌入式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)线程原理-线程同步相关推荐

  1. java 线程 操作系统线程_线程基础:线程(1)——操作系统和线程原理

    1.概述 我在写"系统间通信技术专栏"的时候,收到很多读者的反馈.其中有一部分读者希望我抽空写一写自己关于对Java线程的使用经验和总结.巧的是,这个月我所在的技术团队也有很多同事 ...

  2. 操作系统原理:进程与线程、进程生命周期、线程的类型

    一.进程定义 进程可以看成程序的执行过程,可以展示在当前时刻的执行状态.它是程序在一个数据集合上的一次动态执行的过程.这个数据集合通常包含存放可执行代码的代码段,存放初始化全局变量和初始化静态局部变量 ...

  3. JAVA线程池原理以及几种线程池类型介绍

    在什么情况下使用线程池? 1.单个任务处理的时间比较短      2.将需处理的任务的数量大 使用线程池的好处: 1.减少在创建和销毁线程上所花的时间以及系统资源的开销      2.如不使用线程池, ...

  4. java线程池_Java多线程并发:线程基本方法+线程池原理+阻塞队列原理技术分享...

    线程基本方法有哪些? 线程相关的基本方法有 wait,notify,notifyAll,sleep,join,yield 等. 线程等待(wait) 调用该方法的线程进入 WAITING 状态,只有等 ...

  5. python——通信原理,进程与线程

    一.网络编程 1.计算机网络 将地理位置不同的具有独立功能地多台计算机及其外部设备,通过通信线路连接起来,在协议的管理和协调下,实现资源共享和信息传递. 网络编程:用来实现网络互连的不同计算机运行程序 ...

  6. java线程池工作原理和实现原理

    为什么要使用线程池? 1.使用线程池可以复用池中的线程,不需要每次都创建新线程,减少创建和销毁线程的开销: 2.同时,线程池具有队列缓冲策略.拒绝机制和动态管理线程个数,特定的线程池还具有定时执行.周 ...

  7. 线程池工作原理和实现原理

    为什么要使用线程池 平时讨论多线程处理,大佬们必定会说使用线程池,那为什么要使用线程池?其实,这个问题可以反过来思考一下,不使用线程池会怎么样?当需要多线程并发执行任务时,只能不断的通过new Thr ...

  8. 25张图展示线程池工作原理和实现原理,建议认真阅读,对你有帮助

    上篇<这样的API网关查询接口优化,我是被迫的>文章末尾,有朋友留言提到文中的场景是IO密集型操作,不是CPU密集操作,不需要使用线程池,我猜这位朋友可能想表达的是IO密集且阻塞时间久的不 ...

  9. JUC:7_2三大辅助类:CylicBarrier原理及使用、线程加法计数器

    JUC:7_2三大辅助类:CylicBarrier原理及使用.线程加法计数器 JUC:7_1三大辅助类:CountDownLatch原理及使用.线程减法计数器 什么是CyclicBarrier? 构造 ...

最新文章

  1. Traveller项目介绍
  2. 中國批准英特爾在東北投建晶片廠
  3. 在线html转ipa,iphone在线安装 ipa 应用:利用 itms-services 协议实现 iOS 应用程序在线安装功能...
  4. TCP 三次握手 和 四次挥手
  5. 很长很真实!但会对你有所帮助的(关于职业规划)
  6. 2010年十大改变电信业的小趋势
  7. Linux不得不知道的目录和文件
  8. MYSQL读书笔记---运算符、字符串操作
  9. gwt-2.8.2下载_GWT EJB3 Maven JBoss 5.1集成教程
  10. Mockito –带有注释和静态方法的额外接口
  11. 【 HDU - 2594 】Simpsons’ Hidden Talents(KMP应用,求最长前缀后缀公共子串)
  12. mysql将不同行数结果合并成多列_将多行合并到mysql中的一行和多列
  13. 《菜菜的机器学习sklearn课堂》学习笔记 + 课件
  14. Python 网页爬虫 文本处理 科学计算 机器学习 数据挖掘兵器谱 - 数客
  15. NetDevOps常用数据库安装与基本操作--SQL数据库
  16. 拓端tecdat|使用Python和Keras进行主成分分析、神经网络构建图像重建
  17. 常见的大数据术语表(中英对照)
  18. 信息 | 美国留学之计算机专业【转】
  19. 打开计算机不显示磁盘盘符,移动硬盘盘符不显示如何修复
  20. 004_More Control Flow Tools_流程控制语句

热门文章

  1. python正则表达式——验证密码邮箱
  2. python谢尔宾斯三角形_七月的反思
  3. 幼儿园入园必知:运算符和表达式
  4. 2023-03-20 duckdb-Push-Based Execution Model
  5. 2020湖南大学计算机考研分数,湖南大学2020考研分数线_湖南大学2020考研复试分数线 - 考研营...
  6. 07_JavaScript数据结构与算法(七)双向链表
  7. 铝板规格及产品分类、用途知识一览
  8. 6.12 企业内部upp平台(Unified Process Platform)的关键一刻
  9. 1191:6262:流感传染
  10. 摄像机产品经理应该知道的那些光学知识(景深、光圈)——《工程光学》摘抄汇总版第二部分