鍍金池/ 教程/ Python/ Deferreds 全貌
小插曲 Deferred
異步編程模式與Reactor初探
使用Deferred新功能實(shí)現(xiàn)新客戶端
由twisted支持的客戶端
增強(qiáng)defer功能的客戶端
改進(jìn)詩(shī)歌下載服務(wù)器
測(cè)試詩(shī)歌
更加"抽象"的運(yùn)用Twisted
Deferred用于同步環(huán)境
輪子內(nèi)的輪子: Twisted和Erlang
Twisted 進(jìn)程守護(hù)
構(gòu)造"回調(diào)"的另一種方法
Twisted 理論基礎(chǔ)
惰性不是遲緩: Twisted和Haskell
第二個(gè)小插曲,deferred
使用Deferred的詩(shī)歌下載客戶端
Deferreds 全貌
結(jié)束
取消之前的意圖
由Twisted扶持的客戶端
改進(jìn)詩(shī)歌下載服務(wù)器
初識(shí)Twisted

Deferreds 全貌

簡(jiǎn)介

在上一個(gè)部分,我們學(xué)習(xí)了使用生成器構(gòu)造順序異步回調(diào)的新方法, 加上 deferreds,我們現(xiàn)在有兩種將異步操作鏈接在一起的方法.

但是有時(shí)我們需要"并行"的運(yùn)行一組異步操作.由于Twisted是單線程的,它實(shí)際并不會(huì)并發(fā)運(yùn)行,但我們希望使用異步I/O在一組任務(wù)上盡可能快的工作.以我們的詩(shī)歌客戶端為例,它從多個(gè)服務(wù)器同時(shí)下載詩(shī)歌,而不是一個(gè)接一個(gè)的方式.這就是使用Twisted下載詩(shī)歌的全部細(xì)節(jié).

因此所有詩(shī)歌客戶端需要解決這樣一個(gè)問(wèn)題:怎樣得知你啟動(dòng)的所有異步操作都已經(jīng)完成?目前我們通過(guò)將結(jié)果匯總到一個(gè)列表(如客戶端 7.0中的 結(jié)果 列表)并檢查這個(gè)列表的長(zhǎng)度來(lái)解決這個(gè)問(wèn)題.除了收集成功的結(jié)果,我們還必須小心地對(duì)待失敗,否則一個(gè)失敗將使程序以為還有工作需要做而進(jìn)入死循環(huán).

正如你所料,Twisted包含一個(gè)抽象層可以用來(lái)解決這個(gè)問(wèn)題,我們來(lái)看一看.

DeferredList

DeferredList 類使我們可以將一個(gè) defered 對(duì)象列表視為一個(gè) defered 對(duì)象.通過(guò)這種方法我們啟動(dòng)一族異步操作并且在它們?nèi)客瓿珊螳@得通知(無(wú)論它們成功或者失敗).讓我們看一些例子.

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)

如果運(yùn)行它,將得到如下輸出:

Empty List.
Adding Callback.
We got: []

注意以下幾點(diǎn):

  • DeferredList 由一個(gè)Python列表初始化創(chuàng)建而成.在這種情況下,列表是空的,但我們很快將看到列表的元素必須是 Deferred 對(duì)象.
  • DeferredList 本身是一個(gè) deferred (它繼承 Deferred).這意味著你可以像對(duì)待普通 deferred 一樣向其添加回調(diào)和錯(cuò)誤回調(diào).
  • 在以上例子中,回調(diào)被添加時(shí)立即激發(fā),所以 DeferredList 也必須立即激發(fā).我們一會(huì)兒再討論.
  • deferred 列表的結(jié)果本身也是一個(gè)列表(空).

下面看一下 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')

現(xiàn)在我們創(chuàng)建了包含一個(gè) deferred 元素的 DeferredList 列表,得到如下輸出:

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

注意以下幾點(diǎn):

  • 這次 DeferredList 沒(méi)有激發(fā)它的回調(diào),直到我們激發(fā)列表中的 deferred.
  • 結(jié)果同樣是一個(gè)列表,但這次包含一個(gè)元素.
  • 這個(gè)元素是一個(gè)元組,它的第二個(gè)值是列表中 deferred 的結(jié)果.

讓我們向列表添加兩個(gè) 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')]

現(xiàn)在 DeferredList 的結(jié)果非常清晰,至少以我們的使用方式,它是一個(gè)列表,元素個(gè)數(shù)與傳入構(gòu)造器的 deferred 列表元素個(gè)數(shù)相同. 而且結(jié)果列表的元素包含原始的 deferreds 結(jié)果信息,至少當(dāng)這些 deferred 成功返回.這意味著 DeferredList 本身并不激發(fā)直到所有的原始列表中的 deferreds 都被激發(fā). 而且以一個(gè)空列表創(chuàng)建的 DeferredList 會(huì)立即激發(fā),因?yàn)樗恍枰却魏?deferreds.

那么最終結(jié)果列表中的元素順序如何? 考慮以下代碼( 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')

這里我們先激發(fā) d2 然后再激發(fā) d1,注意構(gòu)造參數(shù)中的 deferred 列表里 d1, d2 仍是原先的順序.輸出結(jié)果如下:

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

輸出列表中結(jié)果的順序與原始 deferred 列表順序相對(duì)應(yīng),而不是 deferred 碰巧被激發(fā)的順序.這一點(diǎn)非常好,因?yàn)槲覀兛梢院苋菀椎貙⒚總€(gè)結(jié)果與生成它的相應(yīng)的操作聯(lián)系在一起(如哪首詩(shī)來(lái)自哪個(gè)服務(wù)器).

好了,那如果列表中一個(gè)或多個(gè) deferreds 失敗了怎么辦呢? 上面結(jié)果中的 True 有什么用? 再看一個(gè)例子(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'))

現(xiàn)在我們以正常結(jié)果激發(fā) d1,以錯(cuò)誤激發(fā) d2.先暫時(shí)忽略 consumerErrors 選項(xiàng),稍候介紹.這里是輸出結(jié)果:

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

這次對(duì)應(yīng) d2 的元組在第二個(gè)位置出現(xiàn)了一個(gè) Failure,并且第一個(gè)位置是 False.至此 DeferredList 的工作原理非常清晰(但繼續(xù)瀏覽以下討論):

  • DeferredList 是以一個(gè) deferred 對(duì)象列表創(chuàng)建的.
  • DeferredList 本身是一個(gè) deferred,它返回的結(jié)果是一個(gè)列表,長(zhǎng)度與 deferred 列表相同.
  • 當(dāng)原始列表中所有 deferred 被激發(fā)后, DeferredList 將會(huì)被激發(fā).
  • 結(jié)果列表中的每個(gè)元素以相同順序?qū)?yīng)原始列表中相應(yīng)的 deferred.如果某個(gè) deferred 成功返回,相應(yīng)元素是(True,result),如果失敗則為(False,failure).
  • DeferredList 不會(huì)失敗,因?yàn)闊o(wú)論每個(gè) deferred 的返回結(jié)果是什么都會(huì)被集總到結(jié)果列表中(同樣,請(qǐng)看下面討論).

現(xiàn)在讓我們討論一下被傳入 DeferredListconsumeErrors 選項(xiàng),如果我們運(yùn)行以上相同代碼而不傳入此選項(xiàng)(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 垃圾回收時(shí)被生成的,而且它表示最后一個(gè)回調(diào)失敗了.這個(gè)消息告訴我們并沒(méi)有完全捕獲潛在的異步錯(cuò)誤.在我們例子中,它是從哪里來(lái)的呢? 很明顯不是來(lái)自 DeferredList,因?yàn)樗呀?jīng)成功返回了.所以它一定是來(lái)自 d2.

DeferredList 需要知道它所監(jiān)視的 deferred 何時(shí)激發(fā). DeferredList 以通常的方式向每個(gè) deferred 添加一個(gè)回調(diào)和錯(cuò)誤回調(diào). 默認(rèn)地,這個(gè)回調(diào)(或錯(cuò)誤)返回原始結(jié)果(或錯(cuò)誤)在將它們放入最終結(jié)果列表之后.由于錯(cuò)誤回調(diào)返回原始 failure 后將觸發(fā)下一個(gè)錯(cuò)誤回調(diào), d2 在它被激發(fā)后仍然保持失敗狀態(tài).

但是如果我們將 consumeErrors=True 傳遞給 DeferredList, 它將向每個(gè) deferred 添加返回 None 的錯(cuò)誤回調(diào), 即"消耗"掉這個(gè)錯(cuò)誤并且取消警告信息. 我們同樣可以向 d2 添加自己的錯(cuò)誤回調(diào)來(lái)處理錯(cuò)誤,如 deferred-list/deferred-list-7.py.

客戶端 8.0

獲取詩(shī)歌客戶端8.0發(fā)布啦!客戶端使用 DeferredList 去發(fā)現(xiàn)所有詩(shī)歌何時(shí)完成(或失敗).新版客戶端位于 twisted-client-8/get-poetry.py. 同樣,唯一的變化在于 poetry_main, 我們來(lái)看一下重要的變化:

...
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 中的相應(yīng)部分比較.

在客戶端 8.0中,我們不需要 poem_done 回調(diào)和 results 列表.相反,我們把每個(gè)從 get_transformed_poem 返回的 deferred 放入 ds 列表,之后創(chuàng)建一個(gè) DeferredList.由于 DeferredList 不會(huì)在所有詩(shī)歌完成或失敗之前激發(fā),我們僅僅向 DeferredList 添加一個(gè)回調(diào)以便關(guān)閉 reactor. 在我們這個(gè)情況中,沒(méi)有使用 DeferredList 返回的結(jié)果,我們僅僅需要知道所有事情何時(shí)結(jié)束.僅此而已!

討論

可視化 DeferredList 的工作方式:

http://wiki.jikexueyuan.com/project/twisted-intro/images/p18_deferred-list.png" alt="" /> 圖33 DeferredList 的結(jié)果

非常簡(jiǎn)單,真的. 還有一些關(guān)于 DeferredList 的選項(xiàng)我們沒(méi)有涉及,以及那些改變我們以上所描述行為的選項(xiàng).我們?cè)趨⒖季毩?xí)中把這些留給讀者自己探索.

在第十九節(jié)中我們將進(jìn)一步介紹 Deferred 類, 包括 Twisted 10.1.0 提出的最新特性.

參考練習(xí)

  1. 閱讀 DeferredList 的源代碼.
  2. 修改 deferred-list 中的例子去實(shí)現(xiàn)可選的構(gòu)造器參數(shù) fireOnOneCallbackfireOnOneErrback. 實(shí)現(xiàn)你將用其中一個(gè)(或兩個(gè)都使用)的情景.
  3. 你可以使用 DeferredLists 列表創(chuàng)建一個(gè) DeferredList 嗎? 如果是這樣,結(jié)果將是什么?
  4. 修改客戶端8.0在所有詩(shī)歌完成下載前不打印任意信息. 這次你將使用 DeferredList 的結(jié)果.
  5. 定義 DeferredDict 的句法并且實(shí)現(xiàn)它.

參考

本部分原作參見(jiàn): dave @ http://krondo.com/blog/?p=2571

本部分翻譯內(nèi)容參見(jiàn)luocheng @ https://github.com/luocheng/twisted-intro-cn/blob/master/p18.rst