[Twisted] Part 8: Deferred Poetry

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

Client 4.0

現在我們對deferreds已經有些許的了解了,我們可以使用它們來重寫我們的Twisted詩歌用戶端。你能在twisted-client-4/get-poetry.py找到4.0版用戶端。

我們的get_poetry函式已經不再需要callback或errback引數了。相反的,它會回傳一個使用者可以根據需要附加callbacks與errbacks的新deferred實例。
def get_poetry(host, port):
    """
    Download a poem from the given host and port. This function
    returns a Deferred which will be fired with the complete text of
    the poem or a Failure if the poem could not be downloaded.
    """
    d = defer.Deferred()
    from twisted.internet import reactor
    factory = PoetryClientFactory(d)
    reactor.connectTCP(host, port, factory)
    return d

我們的factory物件使用deferred實例取代一對callback/errback來進行初始化。一旦我們得到了這首詩歌,或者我們發現我們無法連接伺服器,deferred實例就會被一首詩歌或一個失敗的訊息觸發:
class PoetryClientFactory(ClientFactory):

    protocol = PoetryProtocol

    def __init__(self, deferred):
        self.deferred = deferred

    def poem_finished(self, poem):
        if self.deferred is not None:
            d, self.deferred = self.deferred, None
            d.callback(poem)

    def clientConnectionFailed(self, connector, reason):
        if self.deferred is not None:
            d, self.deferred = self.deferred, None
            d.errback(reason)

注意在deferred實例觸發後我們釋放我們對deferred實例的參考的方式。在Twisted的原始碼中可以在幾個地方找到這個模式(pattern),這有助於確保我們不會觸發同樣的deferred實例兩次。這也讓Python收集垃圾的生活過得更簡單一點。

這裡仍然不需要改變PoetryProtocol,維持原樣就好了。剩下的就是更新Poetry_main函式:
def poetry_main():
    addresses = parse_args()

    from twisted.internet import reactor

    poems = []
    errors = []

    def got_poem(poem):
        poems.append(poem)

    def poem_failed(err):
        print >>sys.stderr, 'Poem failed:', err
        errors.append(err)

    def poem_done(_):
        if len(poems) + len(errors) == len(addresses):
            reactor.stop()

    for address in addresses:
        host, port = address
        d = get_poetry(host, port)
        d.addCallbacks(got_poem, poem_failed)
        d.addBoth(poem_done)

    reactor.run()

    for poem in poems:
        print poem

注意我們如何利用deferred實例的連接功能的優點,在我們主要的callback與errbcak外去重構poem_done的調用。

因為deferreds在Twisted中被大量使用,慣例上會使用小寫字母的區域變數d,去保存你正在使用中的deferred實例。對於要長期儲存來使用時,像物件屬性(object attributes),慣例上會使用deferred命名。

Discussion

透過我們新的用戶端,get_poetry的非同步版本只需要接收詩歌伺服器的位址的資訊,與我們的同步版本一樣。同步版本回傳一首詩,而非同步版本回傳deferred物件。回傳deferred物件是Twisted中非同步APIs與使用Twisted撰寫程式的典型代表,同時這指出了deferreds的另一個概念:
A Deferred object represents an “asynchronous result” or a “result that has not yet come”.
(一個Deferred物件代表一個「非同步的結果」或是一個「還沒到來的結果」。)


我們可以在圖13中比較這兩種程式設計的風格:
13.sync%2Bversus%2Basync.png-Part 8: Deferred Poetry

圖13.同步與非同步


一個非同步API透過回傳deferred實例給用戶這個訊息:
I’m an asynchronous function. Whatever you want me to do might not be done yet. But when it is done, I’ll fire the callback chain of this deferred with the result. On the other hand, if something goes wrong, I’ll fire the errback chain of this deferred instead.
(我是一個非同步函式,無論你希望我做什麼,可能還沒完成。但是當它完成,我會觸發這個deferred中包含著結果的callback鏈。另一方面,如果出了什麼錯誤,我會觸發這個deferred的errback鏈作為代替。)


當然函式不會按照程式字面去觸發deferred實例,因為它已經回傳了。相反的,這個函式已經啟動了一系列事件,這些事件最終會導致觸發這個deferred實例。

所以deferreds是一種時移(time-shifting)函式結果的方法,以滿足非同步模式的需求。而一個函式回傳deferred實例表明這個函式是非同步的,deferred代表未來的結果,也代表著結果會被交付的承諾。

一個同步函式回傳deferred實例是可能的,所以技術上deferred實例回傳了值表示這個函式可能是非同步的(譯註:不是必然),將來的部分我們會看到同步函式回傳deferreds的例子。

因為deferred的行為是定義明確且熟悉的(對於一些有Twisted程式設計經驗的人來說),透過由你自己的APIs回傳deferreds,你可以讓其他Twisted程式設計師更容易理解跟使用你的程式碼。如果沒有deferreds,每個Twisted程式或者每個Twisted的內部組件,可能都有自己獨特的方法去管理callbacks,你為了使用它們必須個別去學習。

When You’re Using Deferreds, You’re Still Using Callbacks, and They’re Still Invoked by the Reactor

當第一次學習Twisted時,給deferreds加入比實際有的還要多的功能是一個常見的錯誤。特別是,經常認為將函式加入deferred實例的鏈中就會自動讓函式非同步化。這可能讓你認為可以在Twisted中,藉由用addCallback將os.system加入到deferred來使用os.system。

我認為這種錯誤是因為嘗試學習Twisted之前沒有先學非同步模型而導致。由於典型的Twisted程式碼中使用了大量的deferreds,而且偶爾才提到reactor,這看起來會變成deferreds在做大多的工作。如果你是從頭開始閱讀這份介紹,你應該很清楚事情遠不是這樣。雖然Twisted是由許多部份組合在一起來工作的,但實現非同步模型的主要責任都落在reactor身上。Deferreds是個很有用的抽象,但是我們之前幾個版本的Twisted用戶端都沒有使用到它們。

讓我們來看一下當我們第一個callback被調用時的stack trace。用一個執行中的詩歌伺服器的位置來運行在twisted-client-4/get-poetry-stack.py的範例,你應該得到一些像這樣的輸出:
  File "twisted-client-4/get-poetry-stack.py", line 129, in
    poetry_main()
  File "twisted-client-4/get-poetry-stack.py", line 122, in poetry_main
    reactor.run()

  ... # some more Twisted function calls

    protocol.connectionLost(reason)
  File "twisted-client-4/get-poetry-stack.py", line 59, in connectionLost
    self.poemReceived(self.poem)
  File "twisted-client-4/get-poetry-stack.py", line 62, in poemReceived
    self.factory.poem_finished(poem)
  File "twisted-client-4/get-poetry-stack.py", line 75, in poem_finished
    d.callback(poem) # here's where we fire the deferred

  ... # some more methods on Deferreds

  File "twisted-client-4/get-poetry-stack.py", line 105, in got_poem
    traceback.print_stack()

這跟我們為2.0版本建立的stack trace非常相似,我們在圖14可以最新的trace:
14.A%2Bcallback%2Bwith%2Ba%2Bdeferred.png-Part 8: Deferred Poetry

圖14.deferred實例的callback


再次聲明,這與我們之前Twsited用戶端類似,但視覺表現上看起來變得模糊不清。但我們先不深入分析這張圖。有一個細節圖上沒有反映出來:上面的callback鏈不會交出控制權,直到deferred實例的第一個callback(got_poem)回傳後、第二個callback(poem_done)被調用前。

這跟我們新的stack trace有點不同,將「Twisted code」與「our code」分隔的線條變得有點模糊(譯註:之前的圖片上會有一條線分隔「Twisted code」與「our code」),因為deferred上方的方法實際上是Twisted程式碼。Twisted和使用者的程式碼在callback鏈中這樣交織在較大型的Twisted程式中很常見,這些程式廣泛的使用其他的Twisted抽象。

透過使用deferred實例,我們在Twisted reactor啟動的callback鏈中加入了一些自己的步驟,回想一下關於callback程式設時時的這情況:

  1. 一次只會有一個callback在執行。
  2. 當reactor在執行時,我們的callback不會執行。
  3. 反之亦然,當我們的callback執行時,reactor不會執行。
  4. 如果我們callback阻塞了,整個程式也會阻塞。

在deferred實例中加入一個callback並不會改變上面的事實。特別是,如果一個阻塞的callback加入了deferred實例,他還是會阻塞。這會變成deferred在觸發(d.callback)時阻塞,因此Twisted也會阻塞。我們可以得出以下結論:
Deferreds are a solution (a particular one invented by the Twisted developers) to the problem of managing callbacks. They are neither a way of avoiding callbacks nor a way to turn blocking callbacks into non-blocking callbacks.
(Deferreds是管理callback的解決方案(由Twisted開發人員發明的特殊的解決方法),它們既不是一種避免callbacks的方法,也不是將阻塞式callbacks變為非阻塞式callback的方法。)


我們可以藉由建構有阻塞式callback的deferred實例來證明最後一點。研究一下twisted-deferred/defer-block.py中的範例程式碼。第二個回調使用time.sleep來達到阻塞,如果你執行這個script並觀察print statements順序,你會清楚的看到阻塞的callback也會在內部阻塞deferred實例。

Summary

藉由回傳一個Deferred,函式告訴使用者「我是非同步的」,並提供一個機制(在此處加入你的callbacks與errbacks)在結果到來時獲得一個非同步結果。Deferreds在整個Twisted codebase中廣泛的使用,當你探索Twisted的APIs時,你一定會遇到它們。所以花點心思去熟悉deferreds並自在的使用他們。

4.0版本用戶端是我們第一個使用真正的「Twisted風格」撰寫Twisted詩歌用戶端,它在非同步函是呼叫時使用deferred實例作為回傳值。我們可以使用更多的Twisted APIs來使它更簡潔,但我認為它代表了如何撰寫簡單的Twisted程式時一個非常好的範例,至少在用戶端的程式上。最終我們也會重寫我們的詩歌伺服器。

但我們還沒有完成deferreds。對於一個相對較短的程式碼來說,Deferred類別提供了數量驚人的功能,我們將在Part 9討論更多的功能以及背後的動機。

Suggested Exercises

  1. 更新4.0版本用戶端,如果在給定的一段時間後沒有接收到詩歌就會逾時。以自訂的例外觸發這個案例中deferred的errback。當你這樣做的時候不要忘記關閉連接。
  2. 更新4.0版本用戶端,當詩歌下載失敗時印出該伺服器位址,這樣使用者就知道哪個伺服器是罪魁禍首。不要忘記在附加callbacks與errbacks時你可以加入額外的位置引數與關鍵字引數。

留言