>>> from env_helper import info; info()
页面更新时间: 2023-12-27 10:45:37
运行环境:
Linux发行版本: Debian GNU/Linux 12 (bookworm)
操作系统内核: Linux-6.1.0-16-amd64-x86_64-with-glibc2.36
Python版本: 3.11.2
6.13. 基于生成器的协程及greenlet¶
在前文中,对生成器实现协程卖了个小关子,在这一节,让我们来揭开谜底。不过在此 之前,需要先重温一下协程的概念,以及它的意义。
6.13.1. 协程的概念¶
协程,又称微线程和纤程等,据说源于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
可以从以上代码看到之前的加锁和谦让CPU的硬伤不复存在,但也损失了利用多核 CPU的能力。所以选择线程还是协程,就要看应用场合了。
6.13.2. 基于生成器的协程¶
好,回到主题:协程这东西关Python的生成器什么事?如果你仔细看上面的伪代码, 应该留意到其中出现了两个yield!是的,因为yield能够中止当前代码的执行,相当于“让 出” CPU资源,跟协程的“协作式”理念不谋而合,所以能够实现协程。
>>> def consumer():
>>> while True:
>>> line = yield
>>> print(line.upper())
>>> def producter():
>>> with open('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)
{
processed line 0
"CELLS": [
processed line 1
{
processed line 2
"CELL_TYPE": "CODE",
processed line 3
"EXECUTION_COUNT": NULL,
processed line 4
"METADATA": {},
processed line 5
"OUTPUTS": [],
processed line 6
"SOURCE": []
processed line 7
}
processed line 8
],
processed line 9
"METADATA": {
processed line 10
"KERNELSPEC": {
processed line 11
"DISPLAY_NAME": "PYTHON 3",
processed line 12
"LANGUAGE": "PYTHON",
processed line 13
"NAME": "PYTHON3"
processed line 14
},
processed line 15
"LANGUAGE_INFO": {
processed line 16
"CODEMIRROR_MODE": {
processed line 17
"NAME": "IPYTHON",
processed line 18
"VERSION": 3
processed line 19
},
processed line 20
"FILE_EXTENSION": ".PY",
processed line 21
"MIMETYPE": "TEXT/X-PYTHON",
processed line 22
"NAME": "PYTHON",
processed line 23
"NBCONVERT_EXPORTER": "PYTHON",
processed line 24
"PYGMENTS_LEXER": "IPYTHON3",
processed line 25
"VERSION": "3.7.3"
processed line 26
},
processed line 27
"TOC": {
processed line 28
"BASE_NUMBERING": 1,
processed line 29
"NAV_MENU": {},
processed line 30
"NUMBER_SECTIONS": TRUE,
processed line 31
"SIDEBAR": TRUE,
processed line 32
"SKIP_H1_TITLE": FALSE,
processed line 33
"TITLE_CELL": "TABLE OF CONTENTS",
processed line 34
"TITLE_SIDEBAR": "CONTENTS",
processed line 35
"TOC_CELL": FALSE,
processed line 36
"TOC_POSITION": {},
processed line 37
"TOC_SECTION_DISPLAY": TRUE,
processed line 38
"TOC_WINDOW_DISPLAY": FALSE
processed line 39
}
processed line 40
},
processed line 41
"NBFORMAT": 4,
processed line 42
"NBFORMAT_MINOR": 2
processed line 43
}
processed line 44
依照上文的理念.编写了这些代码,可以看到consumerO是一个生成器函数,它接收 yield表达式的返回值,转换为全大写,并输出到标准输出,然后再次执行yield把CPU交给 主程序。它的执行结果如下(根据内容会有点不同):
[THU OCT 31 17:49:03 2013] [WARN] INIT: SESSION CACHE IS NOT CONFIGURED [HINT:SSLSESSI0NC.ACHE1
processed line 0 HTTPD: COULD NOT RELIABLY DETERMINE THE SERVER'$ FULLY QUALIFIED DOMAIN NAME, USING APPLETEKIMACBOOK-PRO.LOCAL FOR SERVERNAME
processed line 1 [THU OCT 31 17:49:08 2013] [NOTICE] DIGEST: GENERATING SECRET FOR DIGEST AUTHENTICATION …
processed line 2 [THU OCT 31 17:49:08 2013] [NOTICE] DIGEST: DONE 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)
看这个嵌套循环的代码是不是简单了许多?
6.13.3. 使用 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。
6.13.4. 使用 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]
['128.242.240.212', '93.184.216.34', '146.75.112.223']