在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()
调用时,
能够把实参直接传入内层生成器。应用到本例当中,就不需要定义临时容器来保存每一个学生的成绩,
代码复杂性下降许多。下面是假定 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