- 内容提要
- 前言
- 作者简介
- 封面简介
- 第1章 理解高性能 Python
- 第2章 通过性能分析找到瓶颈
- 2.1 高效地分析性能
- 2.2 Julia 集合的介绍
- 2.3 计算完整的 Julia 集合
- 2.4 计时的简单方法——打印和修饰
- 2.5 用 UNIX 的 time 命令进行简单的计时
- 2.6 使用 cProfile 模块
- 2.7 用 runsnakerun 对 cProfile 的输出进行可视化
- 2.8 用 line_profiler 进行逐行分析
- 2.9 用 memory_profiler 诊断内存的用量
- 2.10 用 heapy 调查堆上的对象
- 2.11 用 dowser 实时画出变量的实例
- 2.12 用 dis 模块检查 CPython 字节码
- 2.13 在优化期间进行单元测试保持代码的正确性
- 2.14 确保性能分析成功的策略
- 2.15 小结
- 第3章 列表和元组
- 第4章 字典和集合
- 第5章 迭代器和生成器
- 第6章 矩阵和矢量计算
- 第7章 编译成 C
- 第8章 并发
- 第9章 multiprocessing 模块
- 第10章 集群和工作队列
- 第11章 使用更少的 RAM
- 第12章 现场教训
8.4 tornado
另一个在Python中很频繁使用的异步I/O包是tornado,由Facebook主要为HTTP客户端和服务器端开发。对比gevent,tornado选择使用回调的方式来做异步行为。无论怎样,在3.x发布版中,类似于协程的行为以一种和老代码兼容的方式添加了进来。
在例8-6中,我们实现了和用gevent相同的网络爬虫,但是使用了tornado的I/O循环(tornado版的事件循环)和HTTP客户端。这样省却了我们的麻烦,比如不得不批量化我们的请求并处理其他更多底层代码的方方面面。
例8-6 tornado HTTP爬虫
from tornado import ioloop from tornado.httpclient import AsyncHTTPClient from tornado import gen from functools import partial import string import random AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient", max_clients=100) # ❶ def generate_urls(base_url, num_urls): for i in xrange(num_urls): yield base_url + "".join(random.sample(string.ascii_lowercase, 10)) @gen.coroutine def run_experiment(base_url, num_iter=500): http_client = AsyncHTTPClient() urls = generate_urls(base_url, num_iter) responses = yield [http_client.fetch(url) for url in urls] # ❷ response_sum = sum(len(r.body) for r in responses) raise gen.Return(value=response_sum) # ❸ if __name__ == "__main__": #... initialization ... _ioloop = ioloop.IOLoop.instance() run_func = partial(run_experiment, base_url, num_iter) result = _ioloop.run_sync(run_func) # ❹
❶ 我们可以配置HTTP客户端并挑选我们希望使用的后台库,以及我们想要批量处理的请求数量。
❷ 我们生成了许多futures,接着yield回到I/O循环中。这个函数会继续,responses变量会被所有的futures填充,当它们就绪时,产生结果。
❸ 在tornado中的协程由Python的产生器(generators)来支持。为了从它们返回值,我们必须要生成一个特殊的异常,由gen.coroutine把它转化成一个返回值。
❹ ioloop.run_sync会只在特殊化的函数.ioloop.start()的运行时间段内启动IOLoop,另一方面,启动了一个必须手动停止的IOLoop。
例8-6的tornado代码和例8-4的gevent代码的一个重要差别是在事件循环运行的时候。对于gevent来说,事件循环只有在iwait函数正运行的时候才运行。另一方面,在tornado中,事件循环在整个时间里都运行,并且控制着程序的完全执行流,而不仅仅是异步的I/O部分。
这使得tornado对于主要是I/O密集型,并且大部分程序(如果不是所有程序)是异步的应用来说很理想。tornado所宣称的最大名声就是作为一个高性能的web服务器。事实上,Micha已经编写了基于tornado的数据库和在很多场合需要许多I/O的数据结构[2]。在另一方面,既然gevent对你的整体程序没有要求,它对于主要是基于CPU,然而有时需要重量级I/O的问题是一个理想的解决方案——例如,一个程序在一个数据集上做了很多计算,接着必须把结果送回数据库来存储。这样甚至会变得更简单,因为事实上大多数数据库有简单的HTTP API,意味着你能够使用grequests。
如果我们看看例8-7中更老风格的使用了回调的tornado代码,我们就能发现tornado的事件循环多么有控制力。我们可以看到为了启动代码,我们必须给程序添加入口点到I/O循环中,接着再启动它。然后,为了让程序终止,我们必须小心翼翼地给我们的I/O循环带上stop函数,并且在合适的时候调用它。结果就是,必须显式地带上回调的程序变得负担相当沉重,而且很快就变得不可维护了。这种情况发生的一个原因是回溯不能再持有变量信息,这些信息就是关于哪些函数调用了哪些函数,并且我们怎样进入到一个异常来启动。即使只是要完全知道调用了哪些函数也变得困难,因为我们一直在创建偏函数来填充参数。毫不奇怪,这就是通常所称的“回调地狱”。
例8-7 使用回调的tornado爬虫
from tornado import ioloop from tornado.httpclient import AsyncHTTPClient from functools import partial AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient", max_clients=100) def fetch_urls(urls, callback): http_client = AsyncHTTPClient() urls = list(urls) responses = [] def _finish_fetch_urls(result): # ❶ responses.append(result) if len(responses) == len(urls): callback(responses) for url in urls: http_client.fetch(url, callback=_finish_fetch_urls) def run_experiment(base_url, num_iter=500, callback=None): urls = generate_urls(base_url, num_iter) callback_passthrou = partial(_finish_run_experiment, callback=callback) # ❷ fetch_urls(urls, callback_passthrou) def _finish_run_experiment(responses, callback): response_sum = sum(len(r.body) for r in responses) print response_sum callback() if __name__ == "__main__": # ... initialization ... _ioloop = ioloop.IOLoop.instance() _ioloop.add_callback(run_experiment, base_url, num_iter, _ioloop.stop) # ❸ _ioloop.start()
❸ 我们把_ioloop.stop作为回调传给run_experiment,这样一旦实验完成,它就会为我们关闭I/O循环。
❷ 回调类型的异步代码包含了许多偏函数的创建。这是因为我们常常需要保留我们传送过去的原始回调,即使当前我们需要把运行时转移给其他函数。
❶ 有时候玩弄局部域是一种有必要的作恶,那是为了保持状态而又不扰乱全局命名空间。
gevent和tornado之间另一个有趣的区别是它们内部改变请求调用图的方式。对比图8-5和图8-4,对于gevent的调用图,我们看到一些区域的对角线看上去更细,而另一些区域的对角线看上去变得更粗。更细的区域显示出在发起新的请求之前,我们正在等待旧请求结束的那些时间段。更粗的区域代表我们太忙了,以致无法读取来自那些本应该已经结束的请求的响应。这些类型的区域代表了事件循环不能优化工作的时间段:或者对资源利用不足,或者超负荷使用资源。
另外,tornado的调用图要更均匀得多。这显示出“tornado”能够更好地优化资源使用。这可以归因于许多因素。这里的一个贡献因素就是因为限制并发请求的数量到100的信号量机制是内建于tornado的,它能够更好地分配资源。还包括以更智能的方式来预分配和复用连接。此外,有许多更小的效果是来自模块就它们与内核的通信方式上的选择,这样是为了协调从多异步操作中接收结果。
图8-5 例8-6的HTTP请求时间表
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论