[Twisted] Part 18: Deferreds En Masse

本文由Dave的Part 18: Deferreds En Masse翻譯而成,你可以由Part 1開始閱讀這個系列的文章,也可以在這裡找到整個系列的目錄。

Introduction

在上個章節,我們學習了使用生成器建構連續的非同步callbacks的新方法。因此,包括了deferreds,我們現在有兩種技術將非同步操作鏈接在一起。

然而,有時候我們希望以「並行(parallel)」的方式執行一組(group)非同步操作。由於Twisted是單執行緒的,它們不會真正的同時執行,但重點在我們希望使用非同步I/O,盡快的處理一組任務(tasks)。例如,我們的詩歌用戶端可以同時從多個伺服器下載詩歌,而不是一個伺服器接著另外一個。畢竟,這就是使用Twisted取得詩歌的重點。

因此,我們所有的詩歌用戶端必須解決這個問題:你怎麼知道你啟動的所有非同步操作完成了?到目前為止,我們已經藉由收集我們的結果到一個串列中(像7.0版本用戶端的results串列),並且檢查列表的長度來解決這個問題。我們除了必須小心的收集成功結果之外,還要收集失敗結果,否則一個失敗結果會導致城市認為還有工作要做而永遠執行。

如你所料,Twisted包含了一個抽象讓你可以用來解決這個問題,而我們今天要來看一下。

The DeferredList

DeferredList類別讓我們可以將一個deferred物件串列作為一個deferred實例來處理。這樣我們可以啟動一堆非同步操作,並且只在它們都完成時才得到通知(無論它們成功或失敗)。讓我們看看一些範例。

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)

如果你執行它,你會得到這樣的輸出:
Empty List.
Adding Callback.
We got: []

有些事情要注意:

  • DeferredList實例是由Python串列(list)所建立的。在這個案例串列中是空的,但我們很快就會看到串列的元素全部都必須是Deferred物件。
  • DeferredList實例本身是一個deferred實例(它繼承自Deferred類別)。照表示你可以像一般deferred實例一樣對它加入callbacks與errbacks。
  • 在上面的範例中,我們的callback在加入DeferredList時立刻就被觸發了,所以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')]

更多事情要注意:

  • 這次直到我們觸發串列中的deferred實例之前,DeferredList都沒有觸發它的callback。
  • 結果仍然是一個串列,但現在有了一個元素。
  • 這個元素是一個tuple,它的第二個值是串列中的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的結果是一個串列在這裡就很明顯了,其元素數量與我們傳遞給建構方法(constructor)的deferreds串列相同。而且結果串列的元素包含原本deferreds實例的結果,至少如果deferreds實例都成功的話。這表示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')]

輸出串列的結果與deferreds原始串列的順序相同,而不是與deferreds觸發的順序相同。這點非常好,因為我們可以輕鬆的將每個個別的結果與產生它的操作關聯再一起(例如哪首詩歌來自哪個伺服器)。

好吧,如果串列中有一或多個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。目前先忽略consumeErrors選項,我們稍後會討論它。這裡是輸出:
Firing d1.
Firing d2 with errback.
We got: [(True, 'd1 result'), (False, <twisted.python.failure.Failure <type 'exceptions.Exception'>>)]

現在對應d2的tuple有一個failure物件在第二個槽位(slot),以及一個False值在第一個槽位。到目前對於DeferredList工作原理應該很清楚了(但請看看下面的Discussion):

  • DeferredList是由deferred物件的串列構成。
  • DeferredList本身是deferred實例,它的結果是與deferreds實例的串列相同長度的串列。
  • 在原本串列中的所有deferreds實例被觸發後,DeferredList實例才會被觸發。
  • 結果串列中的每個元素對應到原始串列中相同位置的deferred實例。如果deferred結果為成功,其對應於結果串列的元素為(True, result),如果deferred結果為失敗,則元素為(False, failure)。
  • DeferredList永遠不會失敗,因為無論每個個別deferred的結果為何都會被收集到結果串列中(但同樣請看看下面的Discussion)。

現在讓我們討論一下我們傳給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

如果你還記得,當deferred被垃圾收集並且deferred最後一個callback是失敗的時候會產生「Unhandled error in Deferred」訊息。這個訊息告訴我們,我們沒有捕捉到在我們程式中所有潛在的非同步錯誤。那麼它來自我們範例中的哪裡?它很明顯不是來自DeferredList,因為它成功執行了。所以它必定來自d2。

DeferredList需要知道知道它所監視的每個deferred何時觸發。而DeferredList用一種常用的方式做到這點-藉由向每個deferred加入一個callback與errback。在預設的情況下,callback(和errback)在被放到最終的串列之後會回傳原始的正常結果(或失敗結果)。而由於從errback回傳原始的失敗結果會觸發下一個errback,所以d2在觸發之後還是保持失敗的狀態。

但如果我們傳送consumeErrors=True給DeferredList,DeferredList向每個deferred加入的errback會變成回傳None,因而「消耗」這個錯誤並消除警告訊息。我們也可以向d2加入我們自己的errback來處理錯誤,就像deferred-list/deferred-list-7.py

Client 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 callback與results串列。相反的,我們從get_transformed_poem取回的deferred放入一個串列(ds),然後建立DeferredList實例。由於DeferredList實例不會觸發,直到所有詩歌完成下載或者失敗,我們只對DeferredList加入一個callback來關閉reactor。在這個案例中,我們沒有使用DeferredList回傳的結果,我們只需要知道所有事情何時結束。僅此而已!

Discussion

我們在圖37視覺化DeferredList的工作原理:
37.the%2Bresult%2Bof%2Ba%2BDeferredList.png-Part 18: Deferreds En Masse

圖37.DeferredList實例的結果


很簡單,真的。有幾個DeferredList的選項我們沒有涵蓋,它們會改變我們上面討論的那些行為。我們在下面的練習中將它們留給你去探索。

下個章節,我們將介紹Deferred類別的另一個功能,這是Twisted 10.1.0中最新引入的功能。

Suggested Exercises

  1. 閱讀DeferredList的原始碼。
  2. 修改deferred-list中的範例以可選的建構方法引數fireOnOneCallback和fireOnOneErrback來實驗。想出你會使用其中一個(或兩個)的情境。
  3. 你可以用DeferredList串列來建立DeferredList嗎?如果可以,結果看起來會像什麼?
  4. 修改8.0用戶端讓它直到所有詩歌下載完成前不會印出任何東西。這次你會使用到DeferredList實例的結果。
  5. 定義DeferredDict實例的語意(semantics)然後實現它。

留言