第九部分:第二個小插曲,Deferred
第九部分:第二個小插曲,Deferred
可以從這裏從頭來閱讀這個系列
更多關於回調的知識
稍微停下來再思考一下回調的機制。盡管對於以Twisted方式使用Deferred寫一個簡單的異步程序已經非常了解了,但Deferred提供更多的是只有在比較復雜環境下才會用到的功能。因此,下面我們自己想出一些復雜的環境,以此來觀察當使用回調編程時會遇到哪些問題。然後,再來看看deferred是如何解決這些問題的。
因此,我們為詩歌下載客戶端添加了一個假想的功能。設想一些計算機科學家發明了一種新詩歌關聯算法,
Byronification引擎。這個漂亮的算法根據一首詩歌生成一首使用Lord Byron
class IByronificationEngine(Interface):
def byronificate(poem):
"""
Return a new poem like the original, but in the style of Lord Byron.
Raises GibberishError if the input is not a genuine poem.
"""
像大多數高尖端的軟件一樣,其實現都存在著許多bugs。這意外著除了已知的異常外,這個byronificate
我們還可以假設這個引擎能夠非常快的動作以至於我們可以在主線程中調用到而無需考慮使用reactor。下面是我們想讓程序實現的效果:
1.嘗試下載詩歌
2.如果下載失敗,告訴用戶沒有得到詩歌
3.如果下載到詩歌,則轉交給Byronificate處理引擎一份
4.如果引擎拋出GibberishError,告訴用戶沒有得到詩歌
5.如果引擎拋出其它異常,則將原始式樣的詩歌立給用戶
6.如果我們得到這首詩歌,則打印它
7.結束程序
這裏設計是當遇到GibberishError異常則表示沒有得到詩歌,因此我們直接告訴用戶下載失敗即可。這也許對調試沒什麽用處,但我們的用戶關心的只是我們下載到詩歌沒有。另一方面,如果引擎因為一些其它的原因而出現處理失敗,那麽我們將原始詩歌交給用戶。畢竟,有詩歌呈現總比沒有好,雖然不是用戶想要的
下面是同步模式的代碼:
try:
poem = get_poetry(host, port) # synchronous get_poetry
except:
print >>sys.stderr, ‘The poem download failed.‘
else:
try:
poem = engine.byronificate(poem)
except GibberishError:
print >>sys.stderr, ‘The poem download failed.‘
except:
print poem # handle other exceptions by using the original poem
else:
print poem
sys.exit()
這段代碼可能經過一些重構會更加簡單,但已經足以說明上面的邏輯流程。我們想升級那些最近使用deferred的客戶端來使用這個功能。但這部分內容我準備把它放在第十部分。現在,我們來考慮一下,用版本3.1來實現這個功能,最後一個沒有使用deferred的客戶端。假設我們無需考慮處理異常,那麽只是改變一下got_poem回調即可:
def got_poem(poem):
poems.append(byron_engine.byronificate(poem))
poem_done()
那麽如果byronificate拋出GibberishError異常或其它異常會發生什麽呢?看看第六部分的圖11,我們可以得到:
1.這個異常會傳播到工廠中的poem_finished回調,即激活got_poem的方法
2.由於poem_finished並沒有捕獲這個異常,因此其會傳遞到protocol中的poemReceive函數
3.然後來到connectionLost函數,仍然在protocol中
4.然後就來到Twisted的核心區,最後止步於reactor。
前面已經了解到,reactor會捕獲異常並記錄它而不是“崩潰”掉。但它卻不會告訴用戶我們的詩歌下載失敗的消息。reactor並不知道任何詩歌或GibberishError
s的信息,它只是一段被設計成適應所有網絡類型的通用代碼,即便與詩歌無關的網絡服務。(Dave這裏想強調的是reactor只是做一些具有普遍意義的事情,不會單獨去處理特定的問題,例如這裏原GibberishError
s異常)
註意異常是如何順著調用鏈傳遞到具有通用性代碼區域。並且看到,在got_poem後面任何一步都沒有可望以我們客戶端的具體要求來處理異常的。這與同步代碼中的方式恰恰相反。
圖15揭示了一個同步客戶端的調用棧:
圖15:同步調用棧
main函數是最高層,意味著它可以觸及整個程序,它為什麽要存在,並且它是如何在整體上表現的。典型的,main函數可以觸及到用戶在命令行輸入想讓程序做什麽的參數。並且它還有一個特殊的目的:為一個命令行式的客戶端打印結果。
socket的connet函數,恰恰相反,其為最低層。它所知道的就是提供到指定地址的連接。它並不知道另一端是什麽及我們為什麽要進行連接。但connect有通用性,不管你因為何種服務要進行網絡連接都可以使用它。
get_poetry在中間,它知道要取一些詩歌,但並不知道如果得不到詩歌會發生什麽。因此,從connect拋出的異常會向上傳遞,從低層的具有通用性的代碼區到高層的具有針對性的代碼區,直到其傳遞到知道如何處理這個異常的代碼區。
現在,我們再回來看看對3.1版的假想功能的實現。我們在圖16裏對調用棧進行了分析,當然只是說明了其中關鍵的函數:
圖16 異步調用棧
現在問題非常清晰了:在回調中,低層的代碼(reactor)調用高層的代碼,其甚至還會調用更高層的代碼。因此一旦出現了異常,它並不會立即被其附件(在調用棧中可觸及)的代碼捕獲,當然附近的代碼也不可能處理它。由於異常每向上傳遞一次,就越靠近低層那些更加不知如何處理該異常的代碼。
一旦異常來到Twisted的核心代碼區,遊戲也就結束了。異常並不會被處理,只是被記錄下來。因此我們在以最原始的回調方式使用回調時(不使用deferred),必須在其進入Twisted之間很好地處理各種異常,至少是我們知道的那些在我們自己設定的規則下會產生的異常。當然其也應該包括那些由我們自己的BUG產生的異常。
由於bug可能存在於我們代碼中的每個角落,因此我們必須將每個回調都放入try/except中,這樣一來所有的異常都才有可能被捕獲。這對於我們的errback同樣適用,因為errback中也可能含有bugs。
Deferred的優秀架構
最終還得由Deferred來幫我們解決這類問題。當一個deferred激活了一個callback或errback時,它就會捕獲各種由回調拋出的異常。換句話說,deferred扮演了try/except模塊,這樣一來,只要我們使用deferred就無需自己來實現這一層了。那deferred是如何解決這個問題的?很簡單,它傳遞異常給在其鏈上的下一個errback。
我們添加到deferred中的第一個errback回調來處理任何出錯信息,信息是在deferred的errback函數調用時發出的。但第二個errback會處理任何由第一個errback或第一個callback拋出的異常,並一直按這種規則傳遞下去。
回憶下圖12.我們假設第一對callback/errback是stage0,下面則是stage1,stage2。。。依次類推。
對於stage N來說,如果其callback或errback出錯,那麽stage N+1的errback就會被調用並收到一個Failure對象作為參數,同時stage N+1的callback就不會被調用了。
通過將回調函數產生的異常向在鏈中傳遞,deferred將異常拋向了高層代碼。這也意味著調用deferred的callback與errback永遠不會在調用都本身處引發異常(只要你僅激活deferred一次),因此,底層的代碼可以放心的激活deferred而無需擔心會引發異常。相反,高層代碼通過向deferred中添加errback(使用addErrback)來捕獲異常。
在同步代碼中,異常會在其被捕獲而停止傳遞,那麽一個errback如何發出其捕獲了異常這一信號呢?同樣很簡單:不再引發異常。這樣一來,執行權就轉移到了callback中來。因此對於stage N來說,不管是callback還是errback成功執行而沒有拋出異常,那麽stage N+1的callback就會被調用,同樣,stage N+1的errback就不會被調用了。
我們來總結一下吧:
1.一個deferred有一個callback/errback對鏈,它們以添加到deferred中的順序依次排列
2.stage 0,即第一對errback/callbac,會在deferred激活時調用,具體調用那個看激活deferred的方式,若是通過.errback激活,則調用errback;同樣若是通過.callback激活則調用callback。(這裏的errback/callback實際是指通過addBoth添加的函數)
3.如果stage N執行出現異常,則stage N+1的errback被調用,並且其參數即為stage N出現的異常
4.同樣,如果stage N成功,即沒有拋出異常,則N+1的callback被調用,其第一個參數為stage N的返回值。
圖17更加直觀的描述上述操作:
圖17:deferred中的控制流程
綠色的線表示callback和errback成功執行沒拋出異常,而紅線表示出現了異常。這些線不僅說明了控制流程還說明了異常與返回值在鏈中流動的情況。圖17顯示了所有deferred能出現的可能路徑,但實際只有一條路徑會存在。圖18顯示了一條可能的路徑:
圖18:可能的deferred激活路線
圖18中,deferred的.callback函數被調用了,因此激活了stage 0的callback。這個callback成功的執行而沒有拋出異常,因此控制權傳給了stage 1的callback。但這個callbac執行失敗而拋出異常,因此控制權傳給了stage 2的errback。errback成功的處理了異常,而沒有再拋出異常,因此控制權傳給了stage 3的callback,並且將errback的返回值作為第一個參數傳了進來(即stage 3的callback中)。
圖18中,可以看出,最後一個stage上的所有的回調出現異常時,都由下一層的errback來捕獲並處理,但如果最後一個stage的callback或errback執行失敗而拋出異常,怎麽辦呢?那麽這個異常就會成為unhandled(未處理)。
在同步代碼中,未處理的異常會導致解釋器崩潰,在原始方式使用回調的代碼中未處理異常會由reactor捕獲並記錄下來。那麽未處理異常出現在deferred中會怎樣呢?讓我們來做個試驗。運行twisted-deferred/defer-unhandled.py試試。下面是輸出:
Finished
Unhandled error in Deferred: Traceback (most recent call last):
...
--- <exception caught here> ---
...
exceptions.Exception: oops
如下幾點需要引起我們的註意:
1.最後一個print函數成功執行,意味著程序並沒有因為出現未處理異常而崩潰。
2.其只是將跟蹤棧打印出來,而沒有宕掉解釋器
3.跟蹤棧的內容告訴我們deferred在何處捕獲了異常
4.“’Unhandle”的字符在“Finished”之後出現。
之所以出現第4條是因為,這個消息只有在deferred被垃圾回收時才會打印出來。我們將在下面的部分看到其中的原因。
在同步代碼中,我們可以使用raise來重新拋出一個異常而無需其它參數。同樣,我們也可以在errback中這樣做。deferred通過以下兩點來判斷callback/errback是否執行成功:
1.callback/errback “raise”一個異常,或
2.callbakc/errback返回一個Failure對象
因為errback的第一個參數就是一個Failure,因此一個errback可以在進行完其處理後可以再次拋出這個Failure。
Callbacks與Errbacks,成對出現
上面討論內容中的一個問題必須要清楚:你添加callback與errback到一個defered的順序會決定這個deferred的的整體運行情況。另一個必須搞清楚的是:在一個deferred中callback與errback往往是成對出現。有四個方法可以向一個deferred的回調鏈中添加callback/errback對:
-
addCallbacks
-
addCallback
-
addErrback
-
addBoth
很明顯的是,第一個與第四個是向鏈中添加函數對。當然中間
兩個也向鏈中添加函數對。
AddCallback
向鏈中添加一個顯式的
callback
函數與一個隱式的”
pass-through“
函數(實在想不出一個對應的詞)。一個
pass-through
函數只是虛設的函數,只將其第一個參數返回。由於
errback
回調函數的第一個參數是
Failure
,因此一個“
path-through”
的
errback
總是執行“失敗”,即將異常傳給下個
errback
回調。
deferred
模擬器
這部分內容,沒有譯。其主要是幫助理解deferred,但你會發現,讀其中的代碼,根本更好的理解deferred。主要是我還沒有理解,嘿嘿。所以就不知為不知吧。
總結
經過這些對回調的考慮,發現由於回調式編程改變了低層代碼與高層代碼的關系,因此讓回調產生的異常直接拋到棧中並不件好事。Deferred通過將異常捕獲然後將其順著回調鏈傳遞來解決了這個問題。
我們同樣意識到,原始數據(返回值)在鏈中被傳遞。結合這個兩事實也就帶來了這樣一種場景:根據每個stage收到的結果的不同,deferred在callback與errback鏈中來回交錯傳遞數據並執行。
我們將在第十部分使用些學到的知識來更新我們的客戶端。
第九部分:第二個小插曲,Deferred