返回介绍

8.4 tornado

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

另一个在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 技术交流群。

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

发布评论

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