协程(coroutine),又称微线程,纤程,是一种用户级的轻量级的线程。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的是时候,恢复先前保存的寄存器上下文和栈,因此协程能保留上次调用的状态,每次过程重入时,就相当于进入上一次调用的状态。在并发编程中,协程和线程类似,每一个协程表示一个执行单元,有自己的本地数据,与其他协程共享全局数据和其他资源。
协程需要用户自己编写调度逻辑,对于cpu来说,协程其实是单线程,所以cpu不用去考虑怎么调度、切换上下文,这就省去了cpu的切换开销,所以协程在一定程度上有好于多线程。
Python通过yield提供对协程的基本支持,但是不完全,而使用第三方gevent库是更好的的选择,gevent提供了比较完善的协程支持,gevent是一个基于协程的Python网络函数,使用greenlet在libev事件循环顶部提供了一个高级别并发性的API。主要特性有以下几点:
- 基于libev的快速事件循环,Linux上是epoll机制
- 基于greelet的轻量级执行单元
- API复用了Python标准库里的内容
- 支持SSL的协作式sokets
- 可通过线程池或c-ares实现DNS查询
- 通过monkey patching功能使得第三方模块变成协作式
gevent对协程的支持,本质上是greenlet在实现切换工作。greenlet工作流程如下:假如进行访问网络的IO操作时,出现阻塞,greenlet就显式切换到另一段没有被阻塞的代码段执行,知道原先的阻塞状况消失以后,再自动切换换回原来的代码段继续处理,因此,greenlet是一种合理安排的窜行方式。
由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO,这就是协程一般比多线程效率高的原因,由于切换实在IO操作时自动完成所以gevent需要修改Python自带的一些标准库,将一些常见的阻塞,如socket、select等地方实现协程跳转,这一过程在启动通过monkey patch完成。
例如:
from gevent import monkey
monkey.patch_all()
import gevent
import requests
def run_task(url):
print('Visit--->{}'.format(url))
try:
response = requests.get(url)
data = response.text
print('bytes received from {}'.format(len(data), url))
except Ellipsis as e:
print(e)
if __name__ == '__main__':
urls = ['https://github.com', 'https://www.python.org', 'http://wwww.cnblogs.com']
greenlets = [gevent.spawn(run_task, url) for url in urls]
gevent.joinall(greenlets)
返回:
以上程序主要用了gevent中的spawn方法和joinall方法,spawn方法可以看做是用来形成协程,joinall方法就是添加这些协程任务,并且启动运行。从运行结果来看,3个网络操作时并发执行的,而且结束顺序不同,但其实只有一个线程。
gevent中还提供了对池的支持,当拥有动态数量的greenlet需要进行并发管理(限制并发数)时,就可以使用池,这在处理大量的网络和IO操作时是非常需要的,接下来使用gevent中pool对象,对上面的例子进行改写,如下:
from gevent import monkey
monkey.patch_all()
import requests
from gevent.pool import Pool
def run_task(url):
print('Visit--->{}'.format(url))
try:
response = requests.get(url)
data = response.text
print('bytes received from {}'.format(len(data), url))
except Ellipsis as e:
print(e)
if __name__ == '__main__':
pool = Pool(2)
urls = ['https://github.com', 'https://www.python.org', 'http://wwww.cnblogs.com']
results = pool.map(run_task, urls)
print(results, 'results')
返回:
通过运行结果可以看出,Pool对象确实对协程的并发数量进行了管理,先访问了前两个网址,当其中一个任务完成时,才会执行第三个。