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