一、进程
进程,保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己独立的地址空间,有自己的堆,上级挂靠单位是操作系统
操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位,是操作系统进行资源分配和调度的一个独立单位
进程一般由程序,数据集合和进程控制块三部分组成
进程状态图
进程间通信(IPC)
- 管道(Pipe)
- 命名管道(FIFO)
- 消息队列(Message Queue)
- 信号量(Semaphore)
- 共享内存(Shared Memory)
- 套接字(Socket)
二、线程
线程,有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)执行的最小单位
线程状态图
三、协程
协程,又称微线程,纤程。英文名Coroutine
一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)
子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。比如子程序A、B:
def A():
print '1'
print '2'
print '3'
def B():
print 'x'
print 'y'
print 'z'
假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:
1
2
x
y
3
z
在A中是没有调用B的,所以协程的调用比函数调用理解起来要难一些
因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能
例子:
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁
如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高
import time
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
time.sleep(1)
r = '200 OK'
def produce(c):
c.next()
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
if __name__=='__main__':
c = consumer()
produce(c)
执行结果:
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK
注意到consumer函数是一个generator(生成器),把一个consumer传入produce后:
- 调用c.next()启动生成器
- 一旦生产了东西,通过c.send(n)切换到consumer执行
- consumer通过yield拿到消息,处理,又通过yield把结果传回
- produce拿到consumer处理的结果,继续生产下一条消息
- produce决定不生产了,通过c.close()关闭consumer,整个过程结束
- 整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务
四、关联图
相互关系
进程与线程
五、区别
1. 调度
- 线程:调度和分配的基本单位
- 进程:拥有资源的基本单位
2. 拥有资源
- 进程:拥有资源的一个独立单位,进程所维护的是程序所包含的资源(静态资源), 如:地址空间,打开的文件句柄集,文件系统状态,信号处理handler等
- 线程:不拥有系统资源,但可以访问隶属于进程的资源。线程所维护的运行相关的资源(动态资源),如:运行栈,调度相关的控制信息,待处理的信号集等
3. 系统开销:
- 进程:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响。一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些
- 线程:线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间。线程的调度需要内核态与用户的频繁切入切出,资源消耗也不小
- 协程:最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。一个线程的内存在 MB 级别,而协程只需要 KB 级别
4. 切换性能消耗
进程切换分两步:
1.切换页目录以使用新的地址空间
2.切换内核栈和硬件上下文
对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2是进程和线程
切换都要做的。
切换的性能消耗:
1.线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换
是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能
损耗是将寄存器中的内容切换出。
2.另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有
已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲会被全部刷新,这将导致内存访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题
5. 实现
- 线程:线程是操作系统的内核对象,多线程编程时,如果线程数过多,就会导致频繁的上下文切换,这些 cpu时间是一个额外的耗费
- 协程:在应用层模拟的线程,他避免了上下文切换的额外耗费,兼顾了多线程的优点。简化了高并发程序的复杂度
6. 堆栈
- 进程:拥有自己独立的堆和栈,既不共享堆,也不共享栈,进程由操作系统调度
- 线程:拥有自己独立的栈和共享的堆,共享堆,不共享栈,标准线程由操作系统调度
- 协程:拥有自己独立的栈和共享的堆,共享堆,不共享栈,协程由程序员在协程的代码里显示调度
假设有一个单核的操作系统,系统上没有其它的程序需要运行,现有两个线程 A 和 B,A 和 B 在单独
运行时都需要10 秒来完成自己的任务,而且任务都是运算操作,线程 A 和 B 之间没有竞争和共享数据
的问题。现在让 A 和 B 两个线程并行,则操作系统会不停的在 A 和 B 两个线程之间切换,达到一种
伪并行的效果
如果操作系统切换的频率是每秒一次,切换的成本是 0.1 秒(主要是栈切换),则总共需
20 + 19 * 0.1 = 21.9 秒;如果使用协程的方式,可以先运行协程 A,A 结束的时候让位给协程 B,
只发生一次切换,则总共需要 20 + 1 * 0.1 = 20.1 秒。如果操作系统是双核的,而且线程是标准线程,
那么线程 A 和 B 可以达到真的并行,则总时间为 10 秒;而协程的方式仍然需要 20.1 秒的时间
六、联系
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源
- 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步
- 进程和线程只在某个cpu上运行(多核系统)