进程、线程、协程 - puwenbin/Wenbin GitHub Wiki

1.什么是进程 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

2.什么是线程 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

3.什么是协程 协程是一种用户态的轻量级线程,操作系统对其存在一无所知程,协程的调度完全由用户控制。又称微线程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。 协程的优点:   (1)无需线程上下文切换的开销,协程避免了无意义的调度,由此可以提高性能(但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力)   (2)无需原子操作锁定及同步的开销   (3)方便切换控制流,简化编程模型   (4)高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。 协程的缺点:   (1)无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。   (2)进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

4.子程序 子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。

子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。

5.线程、进程、协程的区别 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。 协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。 一个应用程序一般对应一个进程,一个进程一般有一个主线程,还有若干个辅助线程,线程之间是平行运行的,在线程里面可以开启协程,让程序在特定的时间内运行。

线程和进程的区别: (1)共享地址空间:线程是进程内的一个执行单元,进程内至少有一个线程,它们共享进程的地址空间,而进程有自己独立的地址空间 (2)资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源 (3)线程是处理器调度的基本单位,但进程不是 (4)二者均可并发执行 (5)每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制

协程和线程的区别: (1)一个线程可以有多个协程,一个进程也可以单独拥有多个协程,这样python中则能使用多核CPU。 (2)线程进程都是同步机制,而协程则是异步 (3)协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态 (4)线程是抢占式调度,协程是协同式调度

6.和多线程相比,协程有哪些优势 (1)协程极高的效率,因为子程序的切换不是线程切换,而是由程序本省控制,因此没有线程切换的开销,和多线程相比,线程数量越多,协程的性能优势就越明显。 (2)不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制资源部加锁,只需判断状态就行。

7.同一进程间的线程究竟共享哪些资源呢,而又各自独享哪些资源呢? 共享的资源有 a. 堆 由于堆是在进程空间中开辟出来的,所以它是理所当然地被共享的;因此new出来的都是共享的(16位平台上分全局堆和局部堆,局部堆是独享的) b. 全局变量 它是与具体某一函数无关的,所以也与特定线程无关;因此也是共享的 c. 静态变量 虽然对于局部变量来说,它在代码中是“放”在某一函数中的,但是其存放位置和全局变量一样,存于堆中开辟的.bss和.data段,是共享的 d. 文件等公用资源 这个是共享的,使用这些公共资源的线程必须同步。Win32 提供了几种同步资源的方式,包括信号、临界区、事件和互斥体。 独享的资源有 a. 栈 栈是独享的 b. 寄存器 这个可能会误解,因为电脑的寄存器是物理的,每个线程去取值难道不一样吗?其实线程里存放的是副本,包括程序计数器PC

线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID。

线程拥有这许多共性的同时,还拥有自己的个性。有了这些个性,线程才能实现并发性。这些个性包括:

(1)线程ID
  每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标

识线程。 (2)寄存器组的值 由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线 程切换到另一个线程上时,必须将原有的线程的寄存器集合的状态保存,以便 将来该线程在被重新切换到时能得以恢复。 (3)线程的堆栈 堆栈是保证线程独立运行所必须的。 线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程 必须拥有自己的函数堆栈,使得函数调用可以正常执行,不受其他线程的影 响。

(4)错误返回码
   由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用

后设置了errno值,而在该线程还没有处理这个错误,另外一个线程就在此时 被调度器投入运行,这样错误值就有可能被修改。 所以,不同的线程应该拥有自己的错误返回码变量。

(5)线程的信号屏蔽码
   由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自

己管理。但所有的线程都共享同样的信号处理器。

(6)线程的优先级
   由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参

数,这个参数就是线程的优先级。

8.多线程中的堆和栈 很多现代操作系统中,一个进程的(虚)地址空间大小为4G,分为系统空间和用户空间两部分,系统空间为所有进程共享,而用户空间是独立的,一般WINDOWS进程的用户空间为2G。 一个进程中的所有线程共享该进程的地址空间,但它们有各自独立的(私有的)栈(stack),Windows线程的缺省堆栈大小为1M。 堆(heap)的分配与栈有所不同,一般是一个进程有一个C运行时堆,这个堆为本进程中所有线程共享,Windows进程还有所谓进程默认堆,用户也可以创建自己的堆。 堆: 是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。

栈:是个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是 thread safe的。操作系统在切换线程的时候会自动的切换栈,就是切换 SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。

9.进程间的通信方式 管道 管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。 特点: (1)它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。 (2)它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。 (3)它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

命名管道 FIFO,也称为命名管道,它是一种文件类型。 (1)FIFO可以在无关的进程之间交换数据,与无名管道不同。 (2)FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

消息队列(两个不相关的进程) 消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。 (1)消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。 (2)消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。 (3)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

信号量 需要确保只有一个进程或线程可以进入临界代码并拥有对资源独占式的访问特权。 信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。 (1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。 (2)信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。 (3)每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。 (4)支持信号量组。

临界区:临界资源的关键代码。

共享内存 允许两个不相关的进程访问同一个逻辑内存。 共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。 (1)共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。 (2)因为多个进程可以同时操作,所以需要进行同步。 (3)信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

10.线程同步常用方式与区别 互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。 同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。

临界资源(CCriticalSection)/关键段 当多个线程访问一个独占性共享资源时,可以使用临界区对象。拥有临界区的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区的线程放弃临界区为止。(临界区只能同一进程中线程使用,不能跨进程使用) 临界资源:同时只允许一个进程使用的资源。 临界区:进程中用于访问临界资源的代码段,又称临界段。

互斥量/互斥锁(CMutex) 互斥量多用于多进程之间的线程互斥,用来确保一个线程独占一个资源的访问。

事件(CEvent)/条件变量 事件机制,则允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。或者按照条件变量的说法,提供线程之间的一种通知机制。

信号量(CSemphore) 当需要一个计数器来限制可以使用某共享资源的线程数目时,可以使用“信号量”对象。 信号量提供对临界资源的安全分配。如果存在多份临界资源,在多个线程争抢临界资源的情况下,向线程提供安全分配临界资源的方法。如果临界资源的数量为1,将退化为锁。

令牌: 一种高级的线程同步的方法。它既提供锁的安全访问临界资源的功能,又利用了条件变量使得线程争夺临界资源时是有序的。

11.协程的实现方式 1.yield实现协程     前文所述“子程序(函数)在执行过程中可以中断去执行别的子程序;别的子程序也可以中断回来继续执行之前的子程序”,那么很容易想到Python的yield,显然yield是可以实现这种切换的。 2.greenlet实现协程   Python的 greenlet就相当于手动切换,去执行别的子程序,在“别的子程序”中又主动切换回来 3.gevent 实现协程   Gevent 是一个第三方库,可以轻松通过gevent实现协程程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。   gevent会主动识别程序内部的IO操作,当子程序遇到IO后,切换到别的子程序。如果所有的子程序都进入IO,则阻塞。

12.Gevent实现原理 当一个Gevent遇到io操作时,比如访问网络,就自动切换到其他的Greenlet,等到io操作完成时,再在适当的时候切换回来继续执行。由于io的操作非常耗时,经常使处于等待状态,有了Gevent自动切换协程,就保证总有Greenlet在运行,而不是等io。 说明: (1)使用Gevent,可以获得极高的性能,但是Gevent只能在unix/linux下运行。 (2)Gevent是基于io切换的协程,编写web app代码不需要引入Gevent的包,也不需要改任何代码,仅在部署的时候用一个支持Gevent的wsgi服务器,立刻就能得到数倍的性能提升。