进程、线程、协程的简介

并发: 任务数大于CPU的核数,多个任务轮流抢占CPU资源执行,由于CPU切换速度特别快,看起来像是一起运行,其实是假象,某一时刻一个CPU核只有一个线程在执行。

并行: 任务数小于或者等于CPU的核数,那么多个任务是真正意义一起执行。

多进程、多线程、IO多路复用

通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作,如select、poll、epoll、kqueue)

例如:Linux系统函数 fork() 可在父进程中创建一个子进程,在父进程接到新请求时,复制出一个子进程来处理,即父进程监控请求,子进程处理,实现并发处理。注意:必须是Linux系统,windows不能用fork。

 

 

进程、线程、协程的故事

很久以前,有两个程序,暂且称他们 旺财(应用程序A)和 小强(应用程序B)吧。

旺财和小强这两个程序都很长,每个都有十几万行。

他们两个的人生价值就是到CPU上去运行,把运行结果告诉人类。

CPU是稀缺资源,只有一个,他们俩必须排着队,轮流使用。

旺财从头到尾执行完了,让出CPU, 让小强从头儿去执行。

人类把这种处理方式叫做批处理。

640?wx_fmt=gif

1、进程 ( Process )

长久以来,两人(两个应用程序)相安无事。 后来CPU的速度越来越快, 远远超过了内存,硬盘的速度。

人类想到,这批处理系统的效率有点低啊,你看当小强需要从硬盘上读取数据的时候,CPU也一直在等待,这是多大的浪费啊!这时候完全可以让旺财来运行一下嘛!

当然得保存好小强的执行现场:具体执行到那一行程序指令了, 函数调用到什么层次了,每个函数调用都有什么样的参数,CPU寄存器中的值..... 等等一系列东西。

如果不把小强的执行现场给保存下来,等到小强的数据从硬盘读完了,就没法回到中断处来继续执行了。

这个执行现场,再加上小强的代码,就是一个执行中的程序,被称为“进程”

旺财和小强在运行的时候,也被改造成了进程。

人类还规定:进程不能长时间占据CPU, 只能在CPU上执行一小会儿,然后马上切换到别的进程去执行。

旺财和小强不以为意:不就是执行一会儿,歇一会儿,然后继续执行嘛!

但是他们不知道的是,由于CPU运行速度超快,旺财和小强虽然在不断地切换运行,在人类那缓慢的世界里看来,旺财和小强好像是同时在执行一样,这就是并发(一起出发并执行,实际某一时刻CPU在轮询执行)。

(在人类看来,小强和旺财似乎是在同时执行)

多年以后,他们俩才真正地实现了并行: 在一个豪华电脑中,每人都被分配了一个CPU , 真正地同时执行(并行), 这是后话了。

进程的几种状态

1)run(运行状态):正在运行的进程或在等待队列中等待的进程,等待的进程只要以得到cpu就可以运行

2)Sleep(可中断休眠状态):相当于阻塞或在等待的状态

3)D(不可中断休眠状态):在磁盘上的进程

4)T(停止状态):这中状态无法直观的看见,因为是进程停止后就释放了资源,所以不会留在Linux中

5)Z(Zombie,僵尸状态):子进程先与父进程结束,但父进程没有调用wait或waitpid来回收子进程的资源,所以子进程就成了僵尸进程,如果父进程结束后任然没有回收子进程的资源,那么1号进程将回收

 

2、线程 ( Thread )

这时候旺财已经有了界面,还能访问网络,每当它联网的时候(这也是个非常非常耗时的操作),就得把CPU让给小强。

即使旺财再次被调度执行,由于网络数据还没有返回,他必须等待,什么事情都做不了,在人类看来,界面根本无法操作,旺财不响应了!  气得人类经常把旺财kill掉。

旺财心里苦,他很纳闷小强怎么就没有问题,小强不是要读写硬盘吗? 那也是很慢的操作啊。

小强说:“你傻啊,内部只有一个执行的流程,一遇到耗时的操作就得等待,你看看我,内部搞了两个执行流程(线程),一个用来读写硬盘(T1),另外一个处理界面(T2)。我和操作系统商量好了,如果T1在读写硬盘, 就可以调度我的T2来执行,这样界面至少还可以操作。 ”

640?wx_fmt=gif

旺财觉得很有意思,也采用了类似办法。

于是,一个进程中至少有一个执行的流程(主线程),也可以开启新的执行流程(线程)

线程变成了CPU的最小调度单位。

线程是CPU调度和分配的基本单位,程序执行的最小单位。

在同一时间片只能有一个线程针对一个CPU执行指令,而且其他的线程必须被挂起。

然后内核调度程序不断的唤醒/挂起线程来模拟多个任务的执行。

一个进程可以由很多个线程组成,线程间共享进程的所有资源。

线程有自己的堆栈、局部变量。

线程的创建调用pthread_create

线程中执行时一般都要进行同步和互斥,保证数据的一致性,因为他们共享同一进程的所有资源。

同步:防止竞争(因同时修改同一个共享变量而导致数据的不一致)

互斥:使用互斥锁防止多个线程同时读写某一块内存区域。互斥锁止允许一个线程进入临界区。

信号量:内存区域只允许固定个数的线程进入,就要使用信号量,防止线程之间产生冲突。信号量许多个线程同时进入临界区。

多线程的死锁

定义:是指由于两个或者多个线程互相持有对方所需要的资源,相互等待资源,处于僵持状态

处理办法:剥夺资源,或杀死其中一个线程

 

多进程与多线程的区别与联系

1)单位:进程是资源(内存、缓存、CPU、硬盘等)分配的最小单位,线程是程序执行/CPU调度的最小单位

2)地址空间:进程有独立地址空间(用于建立数据表来维护代码段、堆栈段和数据段);线程使用相同的地址空间(共享进程中的数据)

3)开销:CPU切换线程及创建线程的开销比进程小(不需要开辟新的地址空间),进程间切换代价大,线程间切换代价小

4)通信:同一进程间的线程间由于共享进程的数据,通信更方便,难点在于处理同步与互斥;而进程间的通信需要以IPC(进程间通信InterProcess Communication,如管道、消息队列、信号量、共享内存)的方式进行通信

5)健壮性:多进程程序更加健壮(多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。)

6)结束的影响:进程的结束会将它所拥有的所有线程销毁,线程的销毁不影响同个进程下的线程

7)私有属性:“线程有自己的私有属性TCB,线程id,寄存器、硬件上下文,而进程也有自己的私有属性进程控制块PCB,这些私有属性是不被共享的,用来标示一个进程或一个线程的标志”

 

3、协程 ( coroutine )

这一天,旺财被一个叫做生产者和消费者的问题折腾地死去活来,

两个线程,一个线程向队列中放数据,另外一个从队列中取数据

处理起两个线程的协作就显得很麻烦,不但需要加锁,还得做好线程的通知和等待。

正在感慨多线程编程之难的时候, 旺财震惊地发现,小强用了一个极为简单的办法把生产者,消费者问题给解决了。

这个方法的代码如下:

# 生产者
def producer(c):   
    #其他代码  
    while True:          
        value = ...生成数据...
        c.send(value)
 
# 消费者
def consumer():    
    #其他代码      
    while True:
        value = yield 
        print(value)
 
c = consumer()
producer(c)

“这....这怎么执行啊,那个yield是怎么回事?”  旺财表示不解。

“简单啊,你看那个生产者,是不是向消费者发送了数据? ” 小强说。

“对啊,然后呢,生产者发送了数据以后,会马上进行下一轮循环吗?”

“这就是关键所在了,”小强说,“ 它们是这么执行的:”

1)生产者发送数据,暂停运行,不进行下一轮循环

2)消费者其实一直在value = yield 那里等待,直到数据到来,现在数据来了,取出处理(value就是生产者发送过来的数据)。

3)消费者在循环中再次yield, 暂停执行。

4)生产者继续下一轮的循环,生成新的消息,发送给消费者。

640?wx_fmt=gif

旺财觉得很吃惊,小强竟然可以让一个正在执行的程序暂停,他不由得问道:“你这个暂停是真的停止了了,还是说只是像Java的yield那样,让出CPU进入了就绪状态? 等待下次调度运行?

“是真的暂停了,程序就停在那里,等待运行控制权从对方那里转移过来。”

“这不是操作系统干的事情吗? ” 旺财更加吃惊了。

“正是这样,” 小强得意地说:“我打算把类似生产者,消费者这样的代码称为‘协程’, 这个协程有个重要的特点,就是完全被我所调度和掌控, 不用操作系统介入(不需要进入内核态,不用用户态与内核态之间的切换)

“这个协程和线程似乎很像啊。每次协程停止执行的时候,也得保存现场,要不然没法恢复执行。” 旺财说。

“是啊,只是协程比线程更加轻量级,操作系统内核不用参与,相当于用户态线程了,协程的开销极小,可以轻松地创建大量的协程来做事情。 对了,也许你注意到了,我这两个协程是'合作式'的,它们两个同一时刻只能有一个在运行。 实际上,我在底层可以用一个线程去执行这两个协程。  ”

旺财表示同意:“不错,既然两个程序可以'合作',那就不用加锁了,也不用在代码里写什么wait和notify了,在程序层面,可以用同步的方式实现异步的功能了! 代码很清晰,我也搞个协程来玩玩吧!”

 

使用yield+协程  实现生产者-消费者模型

传统的生产者-消费者模型是一个线程写消息,一个线程取消息,使用锁机制可能死锁。

如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产。

yield一般配合send()方法使用:

send(value)将value值传递给yield表达式——下一次yield时返回值给send处——send——yield——

def consumer():#消费者
    r = ''#r初始为空字符,启动生成器后变成'200 0K'
    while True:
        n = yield r#接收send的value作为n,下一次运行时返回r给produce中的r
        if not n:#没有生产时不能消费
            return
        print('[CONSUMER]Consuming %s...' % n)
        r = '200 OK'
 
def producer(c):#生产者
    c.send(None)#启动生成器,启动时必须value为None
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER]Producing %s...' % n)
        r = c.send(n)#send将value传递给yield表达式的值,返回下一次运行yield时的值
        print('[PRODUCER]Consumer return: %s' % r)
    c.close()#生产完5个就结束
 
c = consumer()
producer(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

运行过程:

运行分析:

step1:produce(c),运行produce,运行到c.send(None)——运行c即consumer(),启动生成器,value=None,不需要把value给什么yield接收

step2:运行consumer()时运行遇到n=yield r,返回r=''作为n,切换回producer中的原send处

step3:producer中首轮生产者生产n=1,遇到r=c.send(n),将value=n传给yield所在表达式作为n,此时n=1,消费者消费n=1

step4:在consumer()的while True循环,下一次运行yield所在表达式,返回此时的r=‘200 OK’给r

step5:在producer()中进行while的循环,n+1后变成2,进行:“send 切换到yield 再次yield 切换回send”的循环

step6:循环直到n=5,不再进入while,执行c.close()结束

注意:

1.第一次启动生成器c.send(None),value值为None,启动生成器时,调用c函数,不是到某个yield

2.send传值是传给了表达式,和yield后面是什么没有关系,再次遇到yield切换回原来send处。注意切换的位置以及传回的是yield后面的值

3.传回send是要把send的赋值表达式运行一次的,不是从send语句下面开始。

 

 

1、协程是什么

协程,又称微线程,纤程,coroutine 

协程(coroutine)是一种程序运行的方式即在单线程里多个函数并发地执行.

A coroutine is a function that can suspend its execution (yield) until the given given YieldInstruction finishes.

简单理解是, 在协程中的函数, 如果执行过程中遇到了I/O密集型任务, 被给定的YieldInstruction(让出指令)暂停执行(yield), 交出执行权到其他函数, 直到被给定的YieldInstruction(让出指令)结束, 再继续执行。

协程是一个线程执行,两个子过程通过相互协作完成某个任务。协程和子程序调用很像,但协程是在子程序内部中断去执行别的子程序,适当时候返回接着执行,中断有别于函数调用。

 

2、协程与线程的区别

1)由于协程的特性, 协程适合执行大量的I/O 密集型任务, 而线程在这方面弱于协程

2)协程涉及到函数的切换, 多线程涉及到线程的切换, 所以都有执行上下文, 但是协程不是被操作系统内核所管理, 而完全是由程序所控制(也就是在用户态执行), 这样带来的好处就是性能得到了很大的提升, 不会像线程那样需要在内核态进行上下文切换来消耗资源,因此协程的开销远远小于线程的开销

3)同一时间, 在多核处理器的环境下, 多个线程是可以并行的(一个CPU核处理一个线程),但是运行的协程的函数却只能有一个其他的协程的函数都被suspend挂起等待了, 即协程是并发的

4)由于协程在同一个线程中, 所以不需要用来守卫临界区段的同步性原语(primitive)比如互斥锁、信号量等,并且不需要来自操作系统的支持

5)在协程之间的切换不需要涉及任何系统调用或任何阻塞调用

6)通常的线程是抢先式, 而协程是由程序分配执行权

 

3、协程的原理

当出现IO阻塞的时候,由协程的调度器进行调度,通过将数据流立刻yield掉(主动让出),并且记录当前栈上的数据,阻塞完后立刻再通过线程恢复栈,并把阻塞的结果放到这个线程上去跑,这样看上去好像跟写同步代码没有任何差别,这整个流程可以称为coroutine,而跑在由coroutine负责调度的线程称为Fiber。比如Golang里的 go关键字其实就是负责开启一个Fiber,让func逻辑跑在上面。

由于协程的暂停完全由程序控制,发生在用户态上;

而线程的阻塞状态是由操作系统内核来进行切换,发生在内核态上。

因此,协程的开销远远小于线程的开销,也就没有了Context Switch上下文切换选择的开销。

 

4、协程的应用场景

协程的应用场景主要在于 :I/O 密集型任务

这一点与多线程有些类似,但协程调用是在一个线程内进行的,是单线程,切换的开销小,因此效率上略高于多线程;

当程序在执行 I/O 时操作时,CPU 是空闲的,此时可以充分利用 CPU 的时间片来处理其他任务;

在单线程中,一个函数调用,一般是从函数的第一行代码开始执行,结束于 return 语句、异常或者函数执行(也可以认为是隐式地返回了 None );

有了协程,我们在函数的执行过程中,如果遇到了I/O密集型任务,函数可以临时让出控制权,让 CPU 执行其他函数,等 I/O 操作执行完毕以后再收回其他函数的控制权.

 

 

参考推荐:

进程、线程、协程的区别

进程、线程、协程的故事图解

Java 四种线程池

Java 线程同步的七种方法

Java ThreadLocal 原理及应用

PHP 多线程的应用实例