(转) Twisted :第十八部分 Deferreds 全貌

简介

在上一个部分,咱们学习了使用生成器构造顺序异步回调的新方法.这样,包括 deferreds,咱们如今有两种将异步操做连接在一块儿的方法.python

有时,然而,咱们须要"并行"的运行一组异步操做.因为Twisted是单线程的,它实际并不会并发运行,但咱们但愿使用异步I/O在一组任务上尽量快的工做.以咱们的诗歌客户端为例,它从多个服务器同时下载诗歌,而不是一个接一个的方式.这就是使用Twisted下载诗歌的所有特色.react

做为一个结果,全部诗歌客户端须要解决一个问题:你怎样得知你启动的全部异步操做已经完成?目前咱们经过将结果集总到一个列表(如客户端 7.0中的 结果 列表)并检查这个列表的长度来解决这个问题.除了收集成功的结果,咱们还必须当心地对待失败,不然一个失败将使程序进入死循环,觉得还有工做须要作.git

正如你所料,Twisted包含一个抽象层能够用来解决这个问题,咱们来看一看.github


DeferredList

DeferredList 类使咱们能够将一个 defered 对象列表视为一个 defered 对象.经过这种方法咱们启动一族异步操做而且在它们所有完成后得到通知(不管它们成功或者失败).让咱们看一些例子.服务器

在 deferred-list/deferred-list-1.py 中,能够找到以下代码:并发

from twisted.internet import defer

def got_results(res):
    print 'We got:', res

print 'Empty List.'
d = defer.DeferredList([])
print 'Adding Callback.'
d.addCallback(got_results)

若是运行它,将获得以下输出:app

Empty List.
Adding Callback.
We got: []

注意如下几点:异步

  • DeferredList 由Python列表建立.在这种状况下,列表是空的,但咱们很快将看到列表元素必须是 Deferred 对象.学习

  • DeferredList 自己是一个 deferred (它继承 Deferred).这意味着你能够像对待普通 deferred 同样向其添加回调和错误回调.spa

  • 在以上例子中,回调被添加时当即激发,因此 DeferredList 也必须当即激发.咱们一下子将讨论.

  • deferred 列表的结果自己也是一个列表(空).

下面看一下 deferred-list/deferred-list-2.py:

from twisted.internet import defer

def got_results(res):
    print 'We got:', res

print 'One Deferred.'
d1 = defer.Deferred()
d = defer.DeferredList([d1])
print 'Adding Callback.'
d.addCallback(got_results)
print 'Firing d1.'
d1.callback('d1 result')

如今咱们建立了包含一个 deferred 元素的 DeferredList 列表,获得以下输出:

One Deferred.
Adding Callback.
Firing d1.
We got: [(True, 'd1 result')]

注意如下几点:

  • 此次 DeferredList 没有激发它的回调,直到咱们激发列表中的 deferred.

  • 结果一样是一个列表,但此次包含一个元素.

  • 这个元素是一个元组,它的第二个值是列表中 deferred 的结果.

让咱们向列表添加两个 deferreds (deferred-list/deferred-list-3.py):

from twisted.internet import defer

def got_results(res):
    print 'We got:', res

print 'Two Deferreds.'
d1 = defer.Deferred()
d2 = defer.Deferred()
d = defer.DeferredList([d1, d2])
print 'Adding Callback.'
d.addCallback(got_results)
print 'Firing d1.'
d1.callback('d1 result')
print 'Firing d2.'
d2.callback('d2 result')

获得以下输出:

Two Deferreds.
Adding Callback.
Firing d1.
Firing d2.
We got: [(True, 'd1 result'), (True, 'd2 result')]

如今 DeferredList 的结果很是清晰,至少以咱们的使用方式,它是一个列表,元素个数与传入构造器的 deferred 列表元素个数相同. 并且结果列表的元素包含原始的 deferreds 结果信息,至少当这些 deferred 成功返回.这意味着 DeferredList 自己并不激发直到全部的原始列表中的 deferreds 都被激发. 并且以一个空列表建立的 DeferredList 会当即激发,由于它不须要等待任何 deferreds.

那么最终结果列表中的元素顺序如何? 考虑如下代码( deferred-list/deferred-list-4.py):

from twisted.internet import defer

def got_results(res):
    print 'We got:', res

print 'Two Deferreds.'
d1 = defer.Deferred()
d2 = defer.Deferred()
d = defer.DeferredList([d1, d2])
print 'Adding Callback.'
d.addCallback(got_results)
print 'Firing d2.'
d2.callback('d2 result')
print 'Firing d1.'
d1.callback('d1 result')

这里咱们先激发 d2 而后再激发 d1,注意构造参数中的 deferred 列表里 d1, d2 还是原先的顺序.输出结果以下:

Two Deferreds.
Adding Callback.
Firing d2.
Firing d1.
We got: [(True, 'd1 result'), (True, 'd2 result')]

输出列表中结果的顺序与原始 deferred 列表顺序相对应,而不是 deferred 碰巧被激发的顺序.这一点很是好,由于咱们能够很容易地将每一个结果与生成它的相应的操做联系在一块儿(如哪首诗来自哪一个服务器).

好了,那若是列表中一个或多个 deferreds 失败了怎么办呢? 上面结果中的 True 有什么用? 再看一个例子(deferred-list/deferred-list-5.py):

from twisted.internet import defer

def got_results(res):
    print 'We got:', res

d1 = defer.Deferred()
d2 = defer.Deferred()
d = defer.DeferredList([d1, d2], consumeErrors=True)
d.addCallback(got_results)
print 'Firing d1.'
d1.callback('d1 result')
print 'Firing d2 with errback.'
d2.errback(Exception('d2 failure'))

如今咱们以正常结果激发 d1,以错误激发 d2.先暂时忽略 consumerErrors 选项,稍候介绍.这里是输出结果:

Firing d1.
Firing d2 with errback.
We got: [(True, 'd1 result'), (False, <twisted.python.failure.Failure <type 'exceptions.Exception'>>)]

此次对应 d2 的元组在第二个位置出现了一个 Failure,而且第一个位置是 False.至此 DeferredList 的工做原理很是清晰(但继续浏览如下讨论):

  • DeferredList 是以一个 deferred 对象列表建立的.

  • DeferredList 自己是一个 deferred,它返回的结果是一个列表,长度与 deferred 列表相同.

  • 当原始列表中全部 deferred 被激发后, DeferredList 将会被激发.

  • 结果列表中的每一个元素以相同顺序对应原始列表中相应的 deferred.若是那个 deferred 成功返回,相应元素是(True,result),若是失败则为(False,failure).

  • DeferredList 不会失败,由于不管每一个 deferred 的返回结果是什么都会被集总到结果列表中(一样,请看下面讨论).

如今让咱们讨论一下被传入 DeferredList 的 consumeErrors 选项,若是咱们运行以上相同代码而不传入此选项(deferred-list/deferred-list-6.py),则获得如下输出:

Firing d1.
Firing d2 with errback.
We got: [(True, 'd1 result'), (False, >twisted.python.failure.Failure >type 'exceptions.Exception'<<)]
Unhandled error in Deferred:
Traceback (most recent call last):
Failure: exceptions.Exception: d2 failure

若是你还记得,"Unhandled error in Deferred"消息是在 deferred 垃圾回收时被生成的,并且它表示最后一个回调失败了.这个消息告诉咱们并无彻底捕获潜在的异步错误.在咱们例子中,它是从哪里来的呢? 很明显不是来自 DeferredList,由于它已经成功返回了.因此它必定是来自 d2.

DeferredList 须要知道它所监视的 deferred 什么时候激发. DeferredList 以一般的方式向每一个 deferred 添加一个回调和错误回调. 默认地,这个回调(或错误)返回原始结果(或错误)在将它们放入最终结果列表以后.因为错误回调返回原始 failure 后将触发下一个错误回调, d2 在它被激发后仍然保持失败状态.

可是若是咱们将 consumeErrors=True 传递给 DeferredList, 它将向每一个 deferred 添加返回 None 的错误回调, 即"消耗"掉这个错误而且取消警告信息. 咱们一样能够向 d2 添加本身的错误回调来处理错误,如 deferred-list/deferred-list-7.py.


客户端 8.0

获取诗歌客户端8.0发布啦!客户端使用 DeferredList 去发现全部诗歌什么时候完成(或失败).新版客户端位于 twisted-client-8/get-poetry.py. 一样,惟一的变化在于 poetry_main, 咱们来看一下重要的变化:

...
ds = []

for (host, port) in addresses:
    d = get_transformed_poem(host, port)
    d.addCallbacks(got_poem)
    ds.append(d)

dlist = defer.DeferredList(ds, consumeErrors=True)
dlist.addCallback(lambda res : reactor.stop())

你能够与 客户端 7.0 中的相应部分比较.

在客户端 8.0中,咱们不须要 poem_done 回调和 results 列表.相反,咱们把每一个从 get_transformed_poem 返回的 deferred 放入 ds 列表,以后建立一个 DeferredList.因为 DeferredList 不会在全部诗歌完成或失败以前激发,咱们仅仅向 DeferredList 添加一个回调以便关闭 reactor. 在咱们这个状况中,没有使用 DeferredList 返回的结果,咱们仅仅须要知道全部事情什么时候结束.仅此而已!


讨论

可视化 DeferredList 的工做方式:

_static/p18_deferred-list.png

图37: DeferredList 的结果

很是简单,真的. 还有一些关于 DeferredList 的选项咱们没有涉及,以及那些改变咱们以上所描述行为的选项.咱们在参考练习中把这些留给读者本身探索.

在 :doc:`p19` 中咱们将进一步介绍 Deferred 类, 包括 Twisted 10.1.0 提出的最新特性.


参考练习

  1. 阅读 DeferredList 的源代码.

  2. 修改 deferred-list 中的例子去实现可选的构造器参数 fireOnOneCallback 和 fireOnOneErrback. 实现你将用其中一个(或两个都使用)的情景.

  3. 你可使用 DeferredLists 列表建立一个 DeferredList 吗? 若是是这样,结果将是什么?

  4. 修改客户端8.0在全部诗歌完成下载前不打印任意信息. 此次你将使用 DeferredList 的结果.

  5. 定义 DeferredDict 的句法而且实现它.