进程,线程和协程

303 阅读15分钟

进程和线程概念

  • 进程是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
  • 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

进程的组成

进程一般由程序,数据集合和进程控制块三部分组成。

  • 程序用于描述进程要完成的功能,是控制进程执行的指令集;
  • 数据集合是程序在执行时所需要的数据和工作区;
  • 程序控制块(PCB),包含进程的描述信息和控制信息,是进程存在的唯一标志。

进程具有的特征

  • 动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
  • 并发性:任何进程都可以同其他进程一起并发执行;
  • 独立性:进程是系统进行资源分配和调度的一个独立单位;
  • 结构性:进程由程序、数据和进程控制块三部分组成。

进程和线程的区别

  • 性质不同:进程是资源分配的基本单位,线程是cpu执行运算和调度的基本单位;
  • 归属不同:一个操作系统中可以有很多进程,一个进程可以有很多线程;
  • 开销不同:进程创建,销毁和切换的开销都要远大于线程
  • 拥有资源不同:每个进程都拥有自己的内存和资源,一个进程中的线程会共享这些内存和资源
  • 通信方式不同:进程之间可以通过管道,消息队列,共享内存,信号量,以及socket等机制实现通信,线程之间主要通过共享变量及其变种形式进行通信。
  • 控制和影响能力不同:子进程无法控制父进程,一个进程发生异常时一般不会影响其他进程;子线程可以父线程,如果主线程发生异常,会影响其所在进程和其余线程;
  • 扩展能力不同:多进程可以方便地扩展到多机分布式系统上,多线程想要扩展到多台机器上就很困难;
  • CPU利用率不同:进程的cpu利用率低,因为需要额外的上下文切换开销;线程的cpu利用率高,因为切换简单
  • 可靠性不同:进程的可靠性要高于线程。

进程间通信方式

  • 管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
  • 有名管道 (namedpipe) :有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
  • 信号量(semophore ) :信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 消息队列( messagequeue ):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  • 信号 (sinal ) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
  • 共享内存(shared memory ):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两两配合使用,来实现进程间的同步和通信。
  • 套接字(socket ) :套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。

各种通信方式的比较和优缺点:

  • 管道:速度慢,容量有限,只有父子进程能通讯
  • 有名管道(named pipe):任何进程间都能通讯,但速度慢
  • 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题
  • 信号量:不能传递复杂消息,只能用来同步
  • 共享内存:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存

信号量底层是如何工作的

信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是 1,然后 a 进程来访问内存1的时候,我们就把信号量的值设为 0,然后进程b 也要来访问内存1的时候,看到信号量的值为 0 就知道已经有进程在访问内存1了,这个时候进程 b 就会访问不了内存1。所以说,信号量也是进程之间的一种通信方式。

线程间通信方式

  • 锁机制:包括互斥锁、条件变量、读写锁
    • 互斥锁提供了以排他方式防止数据结构被并发修改的方法。
    • 读写锁允许多个线程同时读共享数据,而对写操作是互斥的。
    • 条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。
  • 信号量机制(Semaphore):包括无名线程信号量和命名线程信号量
  • 信号机制(Signal):类似进程间的信号处理,线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。

死锁

多进程改善了系统资源的利用率并提高了系统的处理能力。然而,并发执行也带来了新的问题——死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

死锁产生的原因

  • 系统资源的竞争 通常系统中拥有的不可剥夺资源,其数量不足以满足多个进程运行的需要,使得进程在 运行过程中,会因争夺资源而陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞争 才可能产生死锁,对可剥夺资源的竞争是不会引起死锁的。
  • 进程推进顺序非法 进程在运行过程中,请求和释放资源的顺序不当,也同样会导致死锁。例如,并发进程 P1、P2分别保持了资源R1、R2,而进程P1申请资源R2,进程P2申请资源R1时,两者都 会因为所需资源被占用而阻塞。
  • 信号量使用不当也会造成死锁。 进程间彼此相互等待对方发来的消息,结果也会使得这 些进程间无法继续向前推进。例如,进程A等待进程B发的消息,进程B又在等待进程A 发的消息,可以看出进程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁。

死锁产生的必要条件

产生死锁必须同时满足以下四个条件,只要其中任一条件不成立,死锁就不会发生。

  • 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。即多个线程不能同时使用同一个资源
  • 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
  • 持有并等待条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  • 环路等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, ..., pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, ..., n-1),Pn等待的资源被P0占有。

如何避免死锁

  • 加锁顺序(线程按照一定的顺序加锁)
  • 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
  • 死锁检测

总结

简单来说,死锁问题的产生是由两个或者以上线程并行执行的时候,争夺资源而互相等待造成的。

死锁只有同时满足互斥、持有并等待、不可剥夺、环路等待这四个条件的时候才会发生。

所以要避免死锁问题,就是要破坏其中一个条件即可,最常用的方法就是使用资源有序分配法来破坏环路等待条件。

协程

协程,是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做『用户空间线程』,具有对内核来说不可见的特性。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。

协程的目的

在传统的J2EE系统中都是基于每个请求占用一个线程去完成完整的业务逻辑(包括事务)。所以系统的吞吐能力取决于每个线程的操作耗时。如果遇到很耗时的I/O行为,则整个系统的吞吐立刻下降,因为这个时候线程一直处于阻塞状态,如果线程很多的时候,会存在很多线程处于空闲状态(等待该线程执行完才能执行),造成了资源应用不彻底。

而协程的目的就是当出现长时间的I/O操作时,通过让出目前的协程调度,执行下一个任务的方式,来消除ContextSwitch上的开销。

协程的特点

  • 线程的切换由操作系统负责调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。
  • 线程的默认Stack大小是1M,而协程更轻量,接近1K。因此可以在相同的内存中开启更多的协程。
  • 由于在同一个线程上,因此可以避免竞争关系而使用锁。
  • 适用于被阻塞的,且需要大量并发的场景。但不适用于大量计算的多线程,遇到此种情况,更好实用线程去解决。

线程和协程的区别

比较项线程协程
占用资源初始单位为1MB,固定不可变初始一般为2KB,可随需要而增大
调度所属有OS的内核完成由用户完成
切换开销涉及模式切换,16个寄存器,PC,SP...等寄存器的刷新等只有三个寄存器的值修改-PC/SP/DX
性能问题资源占用太高,频繁创建销毁会带来严重的性能问题资源占用小,不会带来严重的性能问题
数据同步需要用锁等机制确保数据的一致性和可见性不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多

浏览器下的JavaScript

浏览器的内核是多进程的

  • brower进程(主进程)
    • 负责浏览器的页面展示,与用户交互。如前进,后退
    • 页面的前进,后退
    • 负责页面的管理,创建和销毁其他进程
  • GPU进程
    • 3D渲染
  • 插件进程
    • 每种类型的插件对应一个进程,仅当使用该插件时才能创建
  • 浏览器渲染进程(浏览器内核)
    • GUI渲染线程
      • DOM解析, CSS解析,生成渲染树
    • js引擎线程
      • 执行Js代码
    • 事件触发
      • 管理着一个任务队列
    • 异步HTTP请求线程
    • 定时触发器线程 可以看到 js引擎是浏览器渲染进程的一个线程。

浏览器内核中线程之间的关系

  • GUI渲染线程和JS引擎线程互斥
    • js是可以操作DOM的,如果在修改这些元素的同时渲染页面(js线程和ui线程同时运行),那么渲染线程前后获得的元素数据可能就不一致了。
  • JS阻塞页面加载
    • js如果执行时间过长就会阻塞页面

浏览器是多进程的优点

  • 默认新开 一个 tab 页面 新建 一个进程,所以单个 tab 页面崩溃不会影响到整个浏览器。
  • 第三方插件崩溃也不会影响到整个浏览器。
  • 多进程可以充分利用现代 CPU 多核的优势。
  • 方便使用沙盒模型隔离插件等进程,提高浏览器的稳定性。

setTimeout,setInterval,requestAnimationFrame

setTimeout的运行机制

执行该语句时,是立即把当前定时器代码推入事件队列,当定时器在事件列表中满足设置的时间值时将传入的函数加入任务队列,之后的执行就交给任务队列负责。但是如果此时任务队列不为空,则需等待,所以执行定时器内代码的时间可能会大于设置的时间

setTimeout 和 setInterval区别

  • setTimeout: 指定延期后调用函数,每次setTimeout计时到后就会去执行,然后执行一段时间后才继续setTimeout,中间就多了误差,(误差多少与代码的执行时间有关)。
  • setInterval:以指定周期调用函数,而setInterval则是每次都精确的隔一段时间推入一个事件(但是,事件的执行时间不一定就不准确,还有可能是这个事件还没执行完毕,下一个事件就来了).

setInterval存在的问题

JavaScript中使用 setInterval 开启轮询。定时器代码可能在代码再次被添加到队列之前还没有完成执行,结果导致定时器代码连续运行好几次,而之间没有任何停顿。而javascript引擎对这个问题的解决是:当使用setInterval()时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。这确保了定时器代码加入到队列中的最小时间间隔为指定间隔。 但是,这样会导致两个问题:

  • 1、某些间隔被跳过;
  • 2、多个定时器的代码执行之间的间隔可能比预期的小

requestAnimationFrame

requestAnimationFrame是浏览器用于定时循环操作的一个接口,类似于setTimeout,主要用途是按帧对网页进行重绘。

requestAnimationFrame 之前,主要借助 setTimeout/ setInterval 来编写 JS 动画,而动画的关键在于动画帧之间的时间间隔设置,这个时间间隔的设置有讲究,一方面要足够小,这样动画帧之间才有连贯性,动画效果才显得平滑流畅;另一方面要足够大,确保浏览器有足够的时间及时完成渲染。

一般来说,显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。此外,使用这个API,一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。

requestAnimationFrame是在主线程上完成。这意味着,如果主线程非常繁忙,requestAnimationFrame的动画效果会大打折扣。

requestAnimationFrame使用一个回调函数作为参数。这个回调函数会在浏览器重绘之前调用

// requestAnimationFrame()的api;
requestID = window.requestAnimationFrame(callback); 
// 按照1秒钟60次,来模拟requestAnimationFrame
window.requestAnimFrame = (function(){
    return  window.requestAnimationFrame       || 
            window.webkitRequestAnimationFrame || 
            window.mozRequestAnimationFrame    || 
            window.oRequestAnimationFrame      || 
            window.msRequestAnimationFrame     || 
            function( callback ){
            window.setTimeout(callback, 1000 / 60);
        };
})();