返回介绍

8.1 异步编程介绍

发布于 2024-01-25 21:44:08 字数 3504 浏览 0 评论 0 收藏 0

当一个程序进入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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文