协程(coroutine)又称微线程,纤程,是种用户级别的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时候,将寄存器上下文和栈保存到其他地方,等待切换回来的时候恢复,并从之前保存的寄存器上下文 和 栈继续工作。
并发编程中,协程与 线程类似,每个协程表示一个执行单元,有自己的本地数据,与其他协程共享全局数据及资源池。
协程需要操作员单独写调度逻辑,对CPU来说,协程也就是单线程,因此CPU 不需要考虑怎么调度、切换上下文,省去了CPU开销,所以,协程又在一定程度上好于多线程。
python中实现协程:
python使用yield 提供对协程的基本支持,但是,第三方的 gevent库能更好地提供该服务,gevent有比较完善的协程支持。
geve 是基于协程的python网络函数库,使用greenlet 在libev事件循环顶部提供一个有高级别并发性的API。
特点:
(1)基于libev 的快速事件循环,Linux上的epoll机制。
(2)基于greenlet的轻量级执行单元。
(3)API 复用了python标准库的内容。
(4)支持SSL的协作式sockets。
(5)可以通过线程池或者c-ares 实现DNS查询。
(6)通过 monkey patching功能使得第三方模块编程协作式。
gevent支持协程,其实也可以说是greenlet实现的工作切换。
greenlet工作流程如下:如果访问网路的I/O操作出现阻塞时,greenlet就显式切换到另外一个没有被阻塞的代码段执行,直到原先的阻塞状态消失后,再自动切换会原来的代码段继续处理。 可以说,greenlet是在更合理地安排串行工作方式。
同时,由于IO 操作比较耗时,经常是程序处于等待状态,gevent自动切换协程后,能够保证总有greenlet在运行,而不需要等待IO完成,这是协程比一般多线程效率高的原因。
IO操作是自动完成的,所以gevent 需要修改python的一些自带标准库,将一些常见的阻塞,如:socket、select等地方实现协程跳转,这一过程可以通过monkey patch完成。
如下代码可以显示 gevent的使用流程:(python版本: 3.6 操作系统环境: windows10)from gevent import monkey
monkey.patch_all()
import gevent
import urllib.request
def run_task(url):
print("Visiting %s " % url)
try:
response = urllib.request.urlopen(url)
url_data = response.read()
print("%d bytes received from %s " % (len(url_data), url))
except Exception as e:
print(e)
if __name__ == "__main__":
urls = ["https://stackoverflow.com/", "http://www..com/", "http://github.com/"]
greenlets = [gevent.spawn(run_task, url) for url in urls]
gevent.joinall(greenlets)Visiting https://stackoverflow.com/
Visiting http://www..com/
Visiting http://github.com/
46412 bytes received from http://www..com/
54540 bytes received from http://github.com/
251799 bytes received from https://stackoverflow.com/
gevent的spawn方法可以看做是用来形成协程,joinall 方法相当于添加协程任务并启动运行。由结果可以看到,3个网络请求并发执行,而且结束顺序却不一致,但是却只有一个线程。
gevent还提供池。如果拥有动态数量的 greenlet需要进行并发管理,可以使用池 来处理大量的网络请求 及 IO操作。
如下是 gevent的pool对象,修改如上的多网络请求示例:from gevent import monkey
monkey.patch_all()
from gevent.pool import Pool
import urllib.request
def run_task(url):
print("Visiting %s " % url)
try:
response = urllib.request.urlopen(url)
url_data = response.read()
print("%d bytes reveived from %s " %(len(url_data), url))
except Exception as e:
print(e)
return ("%s read finished.." % url)
if __name__ == "__main__":
pool = Pool(2)
urls = ["https://stackoverflow.com/",
"http://www..com/",
"http://github.com/"]
results = pool.map(run_task, urls)
print(results)
Visiting https://stackoverflow.com/
Visiting http://www..com/
46416 bytes reveived from http://www..com/
Visiting http://github.com/
253375 bytes reveived from https://stackoverflow.com/
54540 bytes reveived from http://github.com/
['https://stackoverflow.com/ read finished..', 'http://www..com/ read finished..', 'http://github.com/ read finished..'] 由结果来看,Pool 对象对协程的并发数量进行了管理,先访问前两个,当其中一个任务完成了,再继续执行第三个请求。