python并发编程之进程、线程、协程的调度原理(六)
进程、线程和协程的调度和运行原理总结。
系列文章
进程、线程的调度策略介绍
linux的操作系统详细调度策略可参考:http://blog.csdn.net/gatieme/article/details/51872659
linux中的进程主要有三种调度策略:
-
优先级调度:将进程分为普通进程和实时进程;
-
先进先出(队列)调度:实时进程先创建的先执行,直到遇到io或主动阻塞。
-
轮转调度(时间片):达到一定的CPU执行时间后强制切换;
多进程程序的调度其实还是线程的调度,线程才是CPU调度的基本单位;在同一个进程内线程切换不会产生进程切换,由一个进程内的线程切换到另一个进程内的线程时,将会引起进程切换。
引起进程or线程调度的原因
-
正在执行的进程执行完毕;
-
执行中进程发生阻塞;(如调用sleep)
-
执行中进程调用了P原语操作,从而因资源不足而被阻塞;或调用了v原语操作激活了等待资源的进程队列;
-
执行中进程提出I/O请求后被阻塞;
-
CPU分配的时间片用完;(默认10ms)
-
就绪队列中的某进程的优先级变得高于当前执行进程的优先级,引发强制切换;
linux下python进程or线程调度
如果我们使用python创建了多进程或多线程,可以认为这几个进程或线程是在公平队列(即优先级相同)的实时进程,那么其调度策略是FIFO和RR。
举个例子,假设现在有一个单核的CPU,python程序创建了5个线程,这五个线程会按创建的时间先后进入到一个公平队列中,CPU按先进先出原则开始执行第一个线程,如果遇到IO操作或休眠,或者执行这个线程的时间超过10ms;CUP就会停止当前线程,切换到第二个线程执行直到第五个线程;然后又从第一个线程开始循环,直到所有的线程执行完毕资源被操作系统回收。
当然,切换进程或线程也需要付出代价的,进程切换的代价大于线程。
进程、线程和协程的资源比较
进程:
-
创建一个进程后,每个进程拥有自己独立的内存地址空间,代码段,数据段,BSS段,堆,栈等所有用户空间的信息;
-
多进程中,子进程复制主进程的几乎所有信息,除了pid等特殊信息;
线程:
-
一个进程下多个线程,多个线程共享进程的进程代码段,进程的公有数据(堆),进程的所拥有其他辅助资源;
-
各个线程独立拥有的资源包括:线程id,程序计数器,一个栈,计数器寄存器和栈用来保存线程的执行历史和执行状态。
协程:
-
协程可以看做轻量级的线程,即协程是在线程下开启,多协程在单线程下实现并发,而操作系统最多只能感知到线程,也就是说协程的切换对于操作系统来说是无感知的,属于程序级别的切换;
-
多个协程共享单线程的代码段、公有数据(堆)等;
-
每个协程拥有自己的栈来保存上下文状态,协程的切换开销更小,对操作系统来说,会认为一个开启了多协程的线程一直在计算;
-
协程的优势在于切换的代价更小,因此CPU的有效利用率得到了提高。
-
python的协程主流通过gevent和asyncio模块实现,它们的核心原理都是底层用代码创建事件循环来对多个协程的上下文进行调度;
进程、线程和协程的应用场景
-
python的代码尽量避免使用多线程;
-
如果上下文有一段代码可以分成相对独立的两个部分,如果独立的两个部分是CPU密集型,那么使用多进程;如果是IO密集型,那么使用协程;如果两者都涉及,可以考虑使用子进程中运行协程。
-
业务代码为了快速创建协程或进程,同时增强代码的可读性,推荐使用匿名函数。
from redis import StrictRedis
rs = StrictRedis(host='192.168.1.20', port=6390, db=1)
def get_rs1():
t = time.time()
res = gevent.joinall([gevent.spawn(lambda x: gevent.sleep(2), x=i) for i in range(2)])
ls = [x.get() if x.kwargs['x'] == 1 else x.get() for x in res]
print(ls)
print(time.time() - t)
if __name__ == '__main__':
get_rs1()
# gevent需要判断返回的结果的顺序