稍微停下來再思考一下回調(diào)的機(jī)制。盡管對于以Twisted方式使用Deferred寫一個簡單的異步程序已經(jīng)非常了解了,但Deferred提供更多的是只有在比較復(fù)雜環(huán)境下才會用到的功能。因此,下面我們自己想出一些復(fù)雜的環(huán)境,以此來觀察當(dāng)使用回調(diào)編程時會遇到哪些問題。然后再來看看deferred是如何解決這些問題的。
因此,我們?yōu)樵姼柘螺d客戶端添加了一個假想的功能。設(shè)想一些計算機(jī)科學(xué)家發(fā)明了一種新詩歌關(guān)聯(lián)算法,Byronification引擎。這個漂亮的算法根據(jù)一首詩歌生成一首使用Lord Byron風(fēng)格的詩歌。另外,專家們提供了其Python的接口,即:
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.
"""
像大多數(shù)高尖端的軟件一樣,其實(shí)現(xiàn)存在著許多bugs。這意外著除了已知的異常外,這個byronificate 方法可能會拋出一些專家當(dāng)時沒有預(yù)料到的異常出來。
我們還可以假設(shè)這個引擎能夠非??斓膭幼饕灾劣谖覀兛梢栽谥骶€程中直接同步調(diào)用而無需考慮使用reactor做異步調(diào)用。下面是我們想讓程序?qū)崿F(xiàn)的效果:
這里設(shè)計的是當(dāng)遇到GibberishError異常則表示沒有得到詩歌,因此我們直接告訴用戶下載失敗即可。這也許對調(diào)試沒什么用處,但我們的用戶關(guān)心的只是我們下載到詩歌沒有。另一方面,如果引擎因為一些其它的原因而出現(xiàn)處理失敗,那么我們將原始詩歌交給用戶。畢竟,有詩歌呈現(xiàn)總比沒有好,雖然不是用戶想要的Byron樣式。
下面是同步模式的代碼:
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()
這段代碼可能經(jīng)過一些重構(gòu)會更加簡單,但已經(jīng)足以說明上面的邏輯流程。我們想升級那些最近使用deferred的客戶端來使用這個功能。但這部分內(nèi)容我準(zhǔn)備把它放在第十部分。現(xiàn)在,我們來考慮一下,用版本3.1來實(shí)現(xiàn)這個功能,最后一個沒有使用deferred的客戶端。假設(shè)我們無需考慮處理異常,那么只是改變一下got_poem回調(diào)即可:
def got_poem(poem):
poems.append(byron_engine.byronificate(poem))
poem_done()
那么如果byronificate拋出GibberishError異?;蚱渌惓l(fā)生什么呢?看看第六部分的圖11,我們可以得到:
前面已經(jīng)了解到,reactor會捕獲異常并記錄它而不是"崩潰"掉。但它卻不會告訴用戶我們的詩歌下載失敗的消息。reactor并不知道任何詩歌或GibberishErrors的信息,它只是一段被設(shè)計成適應(yīng)所有網(wǎng)絡(luò)類型的通用代碼,即便與詩歌無關(guān)的網(wǎng)絡(luò)服務(wù)。(Dave這里想強(qiáng)調(diào)的是reactor只是做一些具有普遍意義的事情,不會單獨(dú)去處理特定的問題,例如這里的GibberishErrors異常)
注意異常是如何順著調(diào)用鏈傳遞到具有通用性代碼區(qū)域。并且可以看到,在got_poem后面任何一步都沒有可能以符合我們客戶端的具體要求來處理異常的機(jī)會。這與同步代碼中的方式恰恰相反。
圖15揭示了一個同步客戶端的調(diào)用棧:
http://wiki.jikexueyuan.com/project/twisted-intro/images/p09_sync-exceptions1.png" alt="" />
main函數(shù)是最高層,意味著它可以觸及整個程序,它明白自己存在的位置,并且知道如何處理低層調(diào)用的結(jié)果。典型的,main函數(shù)可以觸及到用戶想讓程序做什么而輸入的命令行參數(shù)。并且它還有一個特殊的目的:為一個命令行式的客戶端打印結(jié)果。
socket的connet函數(shù),恰恰相反,其為最低層。它所知道的就是提供到指定地址的連接。它并不知道另一端是什么以及我們?yōu)槭裁匆M(jìn)行連接。connect作為通用功能,不管你需要連接到哪種服務(wù)器。
get_poetry在中間,它知道要取一些詩歌,但并不知道如果得不到詩歌會發(fā)生什么。因此,從connect拋出的異常會向上傳遞,從低層的具有通用性的代碼區(qū)到高層的具有針對性的代碼區(qū),直到其傳遞到知道如何處理這個異常的代碼區(qū)。
現(xiàn)在,我們再回來看看對3.1版的假想功能的實(shí)現(xiàn)。我們在圖16里對調(diào)用棧進(jìn)行了分析,當(dāng)然只是標(biāo)明了其中關(guān)鍵的函數(shù):
http://wiki.jikexueyuan.com/project/twisted-intro/images/p09_async-exceptions4.png" alt="" />
現(xiàn)在問題非常清晰了:在回調(diào)中,低層的代碼(reactor)調(diào)用高層的代碼,其甚至還會調(diào)用更高層的代碼。因此一旦出現(xiàn)了異常,它并不會立即被其附近(在調(diào)用棧中可觸及)的代碼捕獲,當(dāng)然附近的代碼也不可能處理它。由于異常每向上傳遞一次,就越靠近低層那些通用的底層代碼,所以更加不知如何處理該異常。
一旦異常來到Twisted的核心代碼區(qū),游戲也就結(jié)束了。異常并不會被處理,只是被記錄下來。因此我們在以最原始的回調(diào)方式使用回調(diào)時(不使用deferred),必須在其進(jìn)入Twisted之間很好地處理各種異常,至少是我們知道的那些在我們自己設(shè)定的規(guī)則下會產(chǎn)生的異常。當(dāng)然其也應(yīng)該包括那些由我們自己的BUG產(chǎn)生的異常。
由于bug可能存在于我們代碼中的每個角落,因此我們必須將每個回調(diào)都放入try/except中,這樣一來所有的異常都才有可能被捕獲。這對于我們的errback同樣適用,因為errback中也可能含有bugs。
最終還得由Deferred來幫我們解決這類問題。當(dāng)一個deferred激活了一個callback或errback時,它就會捕獲各種由回調(diào)拋出的異常。換句話說,deferred扮演了try/except模塊,這樣一來,只要我們使用deferred就無需自己來實(shí)現(xiàn)這一層了。那deferred是如何解決這個問題的?很簡單,它傳遞異常給在其鏈上的下一個errback。
我們添加到deferred中的第一個errback回調(diào)來處理任何出錯信息,信息是在deferred的errback函數(shù)調(diào)用時發(fā)出的。但第二個errback會處理任何由第一個errback或第一個callback拋出的異常,并一直按這種規(guī)則傳遞下去。
回憶下圖12.我們假設(shè)第一對callback/errback是stage0,下面則是stage1,stage2。。。依次類推。
對于stage N來說,如果其callback或errback出錯,那么stage N+1的errback就會被調(diào)用并收到一個Failure對象作為參數(shù),同時stage N+1的callback就不會被調(diào)用了。
通過將回調(diào)函數(shù)產(chǎn)生的異常向在鏈中傳遞,deferred將異常拋向了高層代碼。這也意味著調(diào)用deferred的callback與errback永遠(yuǎn)不會在調(diào)用本身處引發(fā)異常(只要你僅激活deferred一次),因此,底層的代碼可以放心的激活deferred而無需擔(dān)心會引發(fā)異常。相反,高層代碼通過向deferred中添加errback(使用addErrback)來捕獲異常。
在同步代碼中,異常會在其被捕獲后停止傳遞,那么一個errback如何發(fā)出其捕獲了異常這一信號呢?同樣很簡單:不再引發(fā)異常。這樣一來,執(zhí)行權(quán)就轉(zhuǎn)移到了callback中來。因此對于stage N來說,不管是callback還是errback成功執(zhí)行而沒有拋出異常,那么stage N+1的callback就會被調(diào)用,同樣,stage N+1的errback就不會被調(diào)用了。
我們來總結(jié)一下吧:
圖17更加直觀的描述上述操作:
http://wiki.jikexueyuan.com/project/twisted-intro/images/p09_deferred-2.png" alt="" />
綠色的線表示callback和errback成功執(zhí)行沒拋出異常,而紅線表示出現(xiàn)了異常。這些線不僅說明了控制流程還說明了異常與返回值在鏈中流動的情況。圖17顯示了所有deferred能出現(xiàn)的可能路徑,但實(shí)際只有一條路徑會存在。圖18顯示了一條可能的路徑:
http://wiki.jikexueyuan.com/project/twisted-intro/images/p09_deferred-31.png" alt="" />
圖18中,deferred的.callback函數(shù)被調(diào)用了,因此激活了stage 0的callback。這個callback成功的執(zhí)行而沒有拋出異常,因此控制權(quán)傳給了stage 1的callback。但這個callback執(zhí)行失敗而拋出異常,因此控制權(quán)傳給了stage 2的errback。errback成功的處理了異常,而沒有再拋出異常,因此控制權(quán)傳給了stage 3的callback,并且將errback的返回值作為第一個參數(shù)傳了進(jìn)來(即stage 3的callback中)。
圖18中,可以看出,每一層stage上的回調(diào)(callback/errback)出現(xiàn)異常時,都由下一層的errback來捕獲并處理,但如果最后一個stage的callback或errback執(zhí)行失敗而拋出異常,怎么辦呢?那么這個異常就會成為unhandled(未處理)。
在同步代碼中,未處理的異常會導(dǎo)致解釋器崩潰,在原始方式使用回調(diào)的代碼中未處理異常會由reactor捕獲并記錄下來。那么未處理異常出現(xiàn)在deferred中會怎樣呢?讓我們來做個試驗。運(yùn)行twisted-deferred/defer-unhandled.py試試。下面是輸出:
Finished
Unhandled error in Deferred:
Traceback (most recent call last):
...
--- <exception caught here> ---
...
exceptions.Exception: oops
如下幾點(diǎn)需要引起我們的注意:
之所以出現(xiàn)第4條是因為,這個消息只有在deferred被垃圾回收時才會打印出來。我們將在下面的部分看到其中的原因。 在同步代碼中,我們可以使用raise來重新拋出一個異常而無需其它參數(shù)。同樣,我們也可以在errback中這樣做。deferred通過以下兩點(diǎn)來判斷callback/errback是否執(zhí)行成功:
由于errback的第一個參數(shù)就是一個Failure,因此一個errback可以在進(jìn)行完處理后再次拋出這個Failure。
上面討論內(nèi)容中的一個問題必須要清楚:你添加callback與errback到一個defered的順序會決定這個deferred的的整體運(yùn)行情況。另一個必須搞清楚的是:在一個deferred中callback與errback往往是成對出現(xiàn)。有四個方法可以向一個deferred的回調(diào)鏈中添加callback/errback對:
addCallbacks
addCallback
addErrback
addBoth
很明顯的是,第一個與第四個是向鏈中添加函數(shù)對。當(dāng)然中間兩個也向鏈中添加函數(shù)對。addCallback向鏈中添加一個顯式的callback函數(shù)與一個隱式的"pass-through"函數(shù)(實(shí)在想不出一個對應(yīng)的詞)。一個pass-through函數(shù)只是虛設(shè)的函數(shù),只將其第一個參數(shù)返回。由于errback回調(diào)函數(shù)的第一個參數(shù)是Failure,因此一個"path-through"的errback總是執(zhí)行"失敗",即將異常傳給下個errback回調(diào)。
這部分內(nèi)容,沒有譯。其主要是幫助理解deferred,但你會發(fā)現(xiàn),讀其中的代碼twisted-deferred/deferred-simulator.py ,可以更好的理解deferred。主要是我還沒有理解,嘿嘿。所以就不知為不知吧。
經(jīng)過這些對回調(diào)的考慮,發(fā)現(xiàn)由于回調(diào)式編程改變了低層代碼與高層代碼的關(guān)系,因此讓回調(diào)產(chǎn)生的異常直接拋到棧中并不是一件好事。Deferred通過將異常捕獲然后將其順著回調(diào)鏈傳遞來解決了這個問題。
我們學(xué)習(xí)到了原始數(shù)據(jù)(返回值)在鏈中如何被很好的傳遞。綜合所述給大家?guī)砹诉@樣一種場景:deferred根據(jù)每一層stage返回的結(jié)果的不同,在callback與errback鏈中來回交錯傳遞數(shù)據(jù)并執(zhí)行。
我們將在第十部分使用這些學(xué)到的知識來更新我們的客戶端。
本部分原作參見: dave @ http://krondo.com/?p=1825
本部分翻譯內(nèi)容參見楊曉偉的博客 http://blog.sina.com.cn/s/blog_704b6af70100q7nc.html