- Preface 前言
- 第1章 引论
- 第2章 编程惯用法
- 第3章 基础语法
- 建议19:有节制地使用 from…import 语句
- 建议20:优先使用 absolute import 来导入模块
- 建议21:i+=1 不等于 ++i
- 建议22:使用 with 自动关闭资源
- 建议23:使用 else 子句简化循环(异常处理)
- 建议24:遵循异常处理的几点基本原则
- 建议25:避免 finally 中可能发生的陷阱
- 建议26:深入理解 None 正确判断对象是否为空
- 建议27:连接字符串应优先使用 join 而不是 +
- 建议28:格式化字符串时尽量使用 .format 方式而不是 %
- 建议29:区别对待可变对象和不可变对象
- 建议30:[]、() 和 {}:一致的容器初始化形式
- 建议31:记住函数传参既不是传值也不是传引用
- 建议32:警惕默认参数潜在的问题
- 建议33:慎用变长参数
- 建议34:深入理解 str() 和 repr() 的区别
- 建议35:分清 staticmethod 和 classmethod 的适用场景
- 第4章 库
- 建议36:掌握字符串的基本用法
- 建议37:按需选择 sort() 或者 sorted()
- 建议38:使用 copy 模块深拷贝对象
- 建议39:使用 Counter 进行计数统计
- 建议40:深入掌握 ConfigParser
- 建议41:使用 argparse 处理命令行参数
- 建议42:使用 pandas 处理大型 CSV 文件
- 建议43:一般情况使用 ElementTree 解析 XML
- 建议44:理解模块 pickle 优劣
- 建议45:序列化的另一个不错的选择 JSON
- 建议46:使用 traceback 获取栈信息
- 建议47:使用 logging 记录日志信息
- 建议48:使用 threading 模块编写多线程程序
- 建议49:使用 Queue 使多线程编程更安全
- 第5章 设计模式
- 第6章 内部机制
- 建议54:理解 built-in objects
- 建议55:init() 不是构造方法
- 建议56:理解名字查找机制
- 建议57:为什么需要 self 参数
- 建议58:理解 MRO 与多继承
- 建议59:理解描述符机制
- 建议60:区别 getattr() 和 getattribute() 方法
- 建议61:使用更为安全的 property
- 建议62:掌握 metaclass
- 建议63:熟悉 Python 对象协议
- 建议64:利用操作符重载实现中缀语法
- 建议65:熟悉 Python 的迭代器协议
- 建议66:熟悉 Python 的生成器
- 建议67:基于生成器的协程及 greenlet
- 建议68:理解 GIL 的局限性
- 建议69:对象的管理与垃圾回收
- 第7章 使用工具辅助项目开发
- 第8章 性能剖析与优化
建议67:基于生成器的协程及 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
可以从以上代码看到之前的加锁和谦让CPU的硬伤不复存在,但也损失了利用多核CPU的能力。所以选择线程还是协程,就要看应用场合了。
好,回到主题:协程这东西关Python的生成器什么事?如果你仔细看上面的伪代码,应该留意到其中出现了两个yield!是的,因为yield能够中止当前代码的执行,相当于“让出”CPU资源,跟协程的“协作式”理念不谋而合,所以能够实现协程。
def consumer(): while True: line = yield print line.upper() def producter(): with open('/var/log/apache2/error_log', 'r') as f: for i, line in enumerate(f): yield line print 'processed line %d' % i c = consumer() c.next() for line in producter(): c.send(line)
依照上文的理念,编写了这些代码,可以看到consumer()是一个生成器函数,它接收yield表达式的返回值,转换为全大写,并输出到标准输出,然后再次执行yield把CPU交给主程序。它的执行结果如下(根据内容会有点不同):
[THU OCT 31 17:49:08 2013] [WARN] INIT: SESSION CACHE IS NOT CONFIGURED [HINT: SSLSESSIONCACHE] processed line 0 HTTPD: COULD NOT RELIABLY DETERMINE THE SERVER'S 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 () >>> a.next() >>> for s in l: ... t = a.send(s) ... >>> t 325
可以看到无端多出来的对t和a的初始化操作非常刺眼,不过代码总算是可以正常工作。如果你尝试想把它封装成一个用以计算一个学生总分的函数,会更加别扭(想象一下在accumulate()中调用其自身,递归生成器?)。这个问题直到Python 3.3增加了yield from表达式以后才得以解决,通过yield from,外层的生成器在接收到send()或throw()调用时,能够把实参直接传入内层生成器。应用到本例当中,就不需要定义临时容器l来保存每一个学生的成绩,代码复杂性下降许多。下面是假定accumulate使用了yield from后的代码:
>>> a = accumulate () >>> a.next() >>> t = 0 >>> for s in students: ... for klass in s: ... t += a.send(klass) ...
看这个嵌套循环的代码是不是简单了许多?回到协程这个主题,因为Python 2.x版本对协程的支持有限,而协程又是非常有用的特性,所以很多Pythonista就开始寻求语言之外的解决方案,并编写了一系列的程序库,其中最受欢迎的莫过于greenlet。
greenlet是一个C语言编写的程序库,它与yield关键字没有密切的关系。greenlet这个库里最为关键的一个类型就是PyGreenlet对象,它是一个C结构体,每一个PyGreenlet都可以看到一个调用栈,从它的入口函数开始,所有的代码都在这个调用栈上运行。它能够随时记录代码运行现场,并随时中止,以及恢复。看到这里,可以发现它跟yield所能够做到的相似,但更好的是它提供从一个PyGreenlet切换到另一个PyGreenlet的机制。最后看一下来自它帮助文件的一个例子,以便对它有个直观的印象。
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()
最后一行跳到test1,输出12,跳到test2,输出56,跳回test1,输出34;然后test1执行完,gr1就死了。然后,最初的gr1.switch()调用返回,所以永远也不会输出78。
协程虽然不能充分利用多核,但它跟异步I/O结合起来以后编写I/O密集型应用非常容易,能够在同步的代码表面下实现异步的执行,其中的代表当属将greenlet与libevent/libev结合起来的gevent程序库,它是当下最受欢迎的Python网络编程库。最后,以使用gevent并发查询DNS的例子作为结束,使用它进行并发查询n个域名,能够获得几乎n倍的性能提升。
>>> 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] ['74.125.79.106', '208.77.188.166', '82.94.164.162']
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论