在Python中,协程(Coroutine)是实现轻量级并发的重要机制。 基于生成器的协程和Greenlet是两种典型的实现方式,分别利用生成器语法和微线程库来管理异步任务。
协程的概念
协程,又称微线程和纤程等,据说源于Simula和Modula-2语言, 现代编程语言基本上都支持这个特性,比如Lua和ruby都有类似的概念。 协程往往实现在语言的运行时库或虚拟机中,操作系统对其存在一无所知, 所以又被称为用户空间线程或绿色线程。又因为大部分协程的实现是协作式而非抢占式的. 需要用户自己去调度,所以通常无法利用多核,但用来执行协作式多任务非常合适。 用协程来做的东西,用线程或进程通常也是一样可以做的,但往往多了许多加锁和通信的操作。 下面基于生产者消费者模型,对抢占式多线程编程实现和协程编程实现进行对比,首先来看使用以下线程的实现(伪代码):
//队列容器
var q := new queue
/消费者後秸
loop
lock (q)
get Item from q
unlock(q)
if item
use this item
else
sleep
//生产者线程
loop
create some new items
lock (q)
add the items to q
unlock(q)
由以上代码可以看到,线程实现至少有两点硬伤:
- 对队列的操作需要有显式/隐式(使用线程安全的队列)的加锁操作。
- 消费者线程还要通过
sleep
把CPU资源适时地“谦让”给生产者线程使用,其中的适时是多久, 基本上只能静态地使用经验值,效果往往不尽如人意。
而使用协程可以比较好地解决这个问题。看一下基于协程的生产者消费者模型实现(伪代码):
// 队列容器
var q := new queue
// 生产者协程
loop
while q is not full
create some new items
add the items to q
yield to consume
//消费者协裎
loop
while q is not empty
remove some items from q
use the items
yield to produce
创建临时文件xx_error_log
with open('xx_error_log','w') as f:
f.write('')
def consumer():
while True:
line = yield
print(line.upper())
def producter():
with open('xx_error_log','r') as f:
for i,line in enumerate(f):
yield line
print('processed line %d' %i)
c = consumer()
next(c)
for line in producter():
c.send(line)
依照上文的理念,编写了这些代码,可以看到 consumer()
是一个生成器函数,
它接收 yield
表达式的返回值,转换为全大写,并输出到标准输出,
然后再次执行 yield
把CPU交给主程序。它的执行结果如下(根据内容会有点不同):
ERROR: CONNECTION FAILED
processed line 0
WARNING: DISK SPACE LOW
processed line 1
CRITICAL: SERVER DOWN
processed line 2
INFO: BACKUP COMPLETED
processed line 3
可以从输出中看到,每输出一行大写的文字后都有一行来自主程序的处理信息,
绝不会像抢占式的多线程程序那样“乱序”,这就是协程的“协”字之由来。
Python 2.x版本的生成 器无法实现所有的协程特性,是因为缺乏对协程之间复杂关系的支持。
比如一个 yield
协程 依赖另一个 yield
协程,且需要由最外层往最内层进行传值的时候,就没有解决办法。
下面就是一个例子:为班级编写一个程序,计算每一个学生的各科总分,并计算班级总分。先尝试编写以下函数:
def accumulate():
tally = 0
while 1:
tally += (yield tally)
考虑到不同的班级有不同数量的科目,不同的班级有不同数量的学生, 所以编写一个生 成器进行计算,它能根据接收到的数值进行计算, 无须预先知道数量。现在想象一下学生的各科成绩表, 可以想象出它是一个二维表,那么代码大槪如下:
l=[]
for s in students:
t = 0
a = accumulate()
a.next()
for c in s:
t = a.send(c)
l.append(t)
t= 0
a = accumulate()
next(a)
for s in l:
t = a.send(s)
t
325
可以看到无端多出来的对 t
和 a
的初始化操作非常刺眼,不过代码总算是可以正常工作。
如果尝试想把它封装成一个用以计算一个学生总分的函数,
会更加别扭(想象一下在 accumulate()
中调用其自身,递归生成器?)。
这个问题直到Python 3.3增加了 yield from
表达式以后才得以解决,
通过 yield from
,外层的生成器在接收到 send()
或 throw()
调用时,
能够把实参直接传入内层生成器。应用到本例当中,就不需要定义临时容器1来保存每一个学生的成绩,
代码复杂性下降许多。下面是假定 accumulate
使用了 yield from
后的代码:
a= accumulate()
next(a)
for s in students:
for klass in s:
t += s.send(klass)
看这个嵌套循环的代码是不是简单了许多?
使用 greenlet
回到协程这个主题,因为Python 2.x版本对 协程的支持有限,而协程又是非常有用的特性,
所以很多 Pythonista
就开始寻求语言之外的 解决方案,并编写了一系列的程序库,
其中最受欢迎的莫过于 greenlet
。
greenlet
是一个C语言编写的程序库,它与 yield
关键字没有密切的关系。
greenlet
这个库里最为关键的一个类型就是 PyGreenkt
对象,它是一个C结构体,
每一个 PyGreenkt
都可以看到一个调用栈,从它的入口函数开始,
所有的代码都在这个调用栈上运行,它能够随时记录代码运行现场,并随时中止或恢复。
看到这里,可以发现它跟 yield
所能够做到的相似,
但更好的是它提供从一个 PyGreenlet
切换到另一个 PyGreenlet
的机制,
最后看一下来 自它帮助文件的一个例子,以便对它有个直观的印象。
sudo apt install python3-greenlet
或
pip3 install greenlet
from greenlet import greenlet
def test1():
print(12)
gr2.switch()
print(34)
def test2():
print(56)
gr1.switch()
print(78)
gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
12 56 34
最后一行跳到 testl
,输出 12
,跳到 test2
,输出56
,跳间testl
,
输出34
;然后tes1
执行完,
gr1
就死了。然后,最初的 gr1.switch()
调用返回,所以永远也不会输出78。
使用 gevent
的实例
协程虽然不能充分利用多核,但它跟异步I/O结合起来以后编写I/O密集型应用非常容场,
能够在同步的代码表面下实现异步的执行,其中的代表当属将 greenlet
与 libevent/libev
结合起来的 gevent
程序库.
它是当下最受欢迎的Python网络编程库。最后,以使用 gevent
并发査询DNS的例子作为结束,
使用它进行并发査询n个域名,能够获得几乎n倍的性能提升。
安装:
sudo apt install -y python3-gevent
或
pip3 install gevent
import gevent
from gevent import socket
urls=['www.google.com','www.example.com','www.python.org']
jobs = [gevent.spawn(socket.gethostbyname,url) for url in urls]
gevent.joinall(jobs,timeout=2)
[job.value for job in jobs]
--------------------------------------------------------------------------- ModuleNotFoundError Traceback (most recent call last) Cell In[31], line 1 ----> 1 import gevent 2 from gevent import socket 3 urls=['www.google.com','www.example.com','www.python.org'] ModuleNotFoundError: No module named 'gevent'