- 内容提要
- 前言
- 作者简介
- 封面简介
- 第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.1 异步编程介绍
当一个程序进入I/O等待时,暂停执行,这样内核就能执行I/O请求相关的低级操作(这叫作一次上下文切换),直到I/O操作完成时才继续。上下文切换是相当重量级的操作。它要求我们保存程序的状态(丢失了我们在CPU层面上任何类型的缓存),放弃使用CPU。之后,当我们允许再次运行时,我们必须花时间在主板上重新初始化程序并准备好继续运行(当然,所有这一切都在幕后发生)。
另一方面,使用并发,典型情况下我们会有一个叫作“事件循环”的东西,来管理我们程序中该运行什么,什么时候运行。实质上,一个事件循环只是需要运行的一个函数列表。在列表顶端的函数得到运行,接着轮到下一个,依次类推。例8-1展示了一个事件循环的简单例子。
例8-1 一个玩具意义上的事件循环
from Queue import Queue from functools import partial eventloop = None class EventLoop(Queue): def start(self): while True: function = self.get() function() def do_hello(): global eventloop print "Hello" eventloop.put(do_world) def do_world(): global eventloop print "world" eventloop.put(do_hello) if __name__ == "__main__": eventloop = EventLoop() eventloop.put(do_hello) eventloop.start()
这可能看上去不像一个巨大的变化。无论如何,当运行I/O任务时,我们能够耦合事件循环和异步(async)I/O操作来获得巨大的收益。这些操作是非阻塞的,意味着如果我们用一个异步程序做一次网络写操作,它会立即返回,尽管写操作还没有发生。当写操作完成时,会触发一个事件,所以我们的程序就得以知晓了。
把这两个概念放在一起,我们就能获得这样一个程序:当请求一个I/O操作时,在等待原来的I/O操作完成期间,可以运行另外的函数。这实质上还允许我们做有意义的运算,不然我们就会处于I/O等待中。
备忘
函数之间的切换会有开销。内核必须花费时间来设置在内存中被调用的函数,我们缓存的状态将会变得不可预测。正因为如此,在你的程序有许多I/O等待时,并发给出了最好的结果——尽管这种切换的确有它的开销,但它要比把I/O等待时间利用起来从而取得的收益要小得多。
使用事件循环编程能采取两种方式:回调或者future。在回调模式中,使用一个通常称之为回调的函数作为输入参数来调用函数。它会使用值来调用回调函数,而不是把值返回出去。这样就设置了长长的调用函数链,每一个函数得到链中前一个函数返回的值。例8-2是一个回调模式的简单例子。
例8-2 回调例子
from functools import partial def save_value(value, callback): print "Saving {} to database".format(value) save_result_to_db(result, callback) # ❶ def print_response(db_response): print "Response from database: {}".format(db_response) if __name__ == "__main__": eventloop.put( partial(save_value, "Hello World", print_response) )
❶ save_result_to_db是一个异步函数,它会立即返回并且结束,允许其他代码运行。无论如何,一旦数据准备好,print_response就会被调用。
另一方面,使用futures,一个异步函数返回一个future结果的promise,而不是实际的结果。正因为如此,我们必须等待被这种类型的异步函数所返回的future完成,并被我们所期待的值所填充(或者在其中做一个yield,或者通过运行一个函数显式地等待值准备好)。在等待future对象被我们所请求的数据填充时,我们能够做其他运算。如果我们把它和生成器(generators)的概念——能够被暂停并且以后能继续执行的函数——耦合起来,我们就能写出看上去形式上很接近串行代码的异步代码:
@coroutine def save_value(value, callback): print "Saving {} to database".format(value) db_response = yield save_result_to_db(result, callback) # ❶ print "Response from database: {}".format(db_response) if __name__ == "__main__": eventloop.put( partial(save_value, "Hello World") )
❶ 在这种情况下,save_result_to_db返回一个Future类型。通过让步(yielding),我们就确保暂停了save_value,直到值准备好了才继续并完成它的操作。
在Python中,协程是作为生成器(generator)来实现的。这很方便,因为生成器(generator)已经有机制来暂停它们的执行并在以后继续运行。所以,在我们的协程中所发生的事情就是产生一个future,事件循环会等待直到那个future把它的值准备好。一旦值准备好了,事件循环会继续执行那个函数,把future的值送还给它。
对于Python 2.7的基于future的并发实现,当我们设法把协程用作实际函数时,事情会变得有点奇怪。记住生成器(generators)不能返回值,所以就有了库处理这个问题的各种各样的方式。
在Python 3.4中,无论如何,已经引入了新的机制来方便地创建协程,并且还是让协程来返回值。
在本章中,我们将分析一个从HTTP服务器抓取数据的网络爬虫,这个HTTP服务器被构造成有延迟。这代表了无论何时当我们处理I/O时会发生的普遍的响应时间延迟。我们首先创建一个串行爬虫,看起来就像天然的针对这个问题的Python解决方案。接着,我们浏览Python 2.7:gevent和tornado这两种解决方案。最后,我们会查看Python 3.4中的asyncio库,看看在Python中异步编程的未来会是什么样的。
备忘
我们实现的Web服务器能够同时支持多个连接。这对于你要运行I/O操作所遇到的大多数的服务来说是真实的——大多数数据库能够同时支持多个请求,大多数Web服务器支持10000多个同时连接。无论如何,当与一个不能同时处理多个连接的服务交互时[1],我们将总是获得与串行情况下相同的性能。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论