Twisted異步編程
這篇文檔介紹了異步編程模型,以及在Twisted中抽象出的Deferred——象征着“承諾了一定會有的”結果,並且可以把最終結果傳遞給處理函數(Python中實現了__call__()
方法的對象都可以稱之為“函數”,方法也是以函數的形式存在的,因此將所有“function”譯作“函數”。——譯者注)。
這篇文檔適合於剛接觸Twisted的讀者,並且熟悉Python編程語言,至少從概念上熟悉網絡的核心概念,諸如服務器、客戶端和套接口。這篇文檔將給你一個對並發編程的上層概觀(交叉執行許多任務),以及Twisted的並發模型:非阻塞編碼或者叫異步編碼。
在討論過包含有Deferred的並發模型之后,將介紹當函數返回了一個Deferred對象時,處理結果的方法。
並發編程介紹
要完成某些計算任務經常需要不少時間,其原因有兩點:
- 任務是計算集中型的(比如,求一個很大整數的所有因數),並且需要相當的CPU時間進行計算;或者
- 任務並不是計算集中型的,但是需要等待某些數據,以產生結果。
等待回應
網絡編程的基本功能就是等待數據。想象你有一個函數,這個函數會總結一些信息並且作為電子郵件發送。函數需要連接到一個遠程服務器、等待服務器的回應、檢查服務器能否處理這封電子郵件、等待回應、發送電子郵件、等待確認信息,然后斷開連接。
這其中任何一步都有可能占用很長時間。你的程序可能使用所有可能模型中最簡單的一個——它實際上只是停下來等着數據的發送和接收,但在這種情況下,它有非常明顯的基本限制:它不能同時發送多封電子郵件;並且在發送電子郵件的時候,它其實什么也做不了。
因此,除了最簡單的之外,所有網絡程序都會避免這種模型。另外還有許多不同的模型,它們都允許你的程序在等待數據以繼續某個任務的同時,繼續做手頭上的其他任務,這些模型你都可以采納。
不等待數據
編寫網絡程序有很多種辦法,主要有這么幾種:
- 在不同的操作系統進程中處理各個連接,這種情況下,操作系統會處理進程調度,比如當一個進程等待的時候,讓其他進程繼續工作;
- 在不同的線程中處理各個連接1,這種情況下,線程框架會處理好諸如“當一個線程等待時,讓其他線程繼續工作”的問題;或者
- 在一個線程中,使用非阻塞的系統調用來處理所有連接。
非阻塞調用
上述第三種模型就是使用Twisted框架時的標准模型:非阻塞調用。
當在一個線程中處理多個連接時,調度就成為了應用程序的責任,而不是操作系統的。調度通常的實現方案是:當連接准備好讀或是寫時,調度系統會調用一個之前注冊過的函數——通常被叫做異步,事件驅動或是基於回調的編程。
在這種模型下,之前發送電子郵件的函數應該是象這樣的:
- 調用一個連接函數,用以連接到遠程服務器;
- 連接函數立刻返回,暗示着當連接建立后,將調用電子郵件發送的通知;並且
- 連接一旦建立,系統就會通知發送電子郵件的函數,連接已經准備就緒。
於我們最初阻塞的步驟相比,上面這種非阻塞的步驟有什么好處呢?當發送電子郵件的函數在連接建立之前無法繼續的時候,程序的其他部分依然可以執行其他任務,比如說開始為其他電子郵件的連接執行類似的步驟。於是,整個程序就不會卡在等待一個連接的建立上。
callback
callback(回調)是通知應用程序數據已經就緒的經典模型。應用程序在調用一個方法,試圖獲取一些數據時,同時提供一個callback函數,當數據就緒時,這個 callback函數會被調用,並且就緒的數據就是調用的參數之一。因此,應用程序應該在callback函數中,繼續執行之前獲取這些數據時,想要執行的任務(當時沒能馬上得到這些數據,所以當時無法執行這些任務——譯者注)。
在同步編程中,一個函數會先請求數據,然后等待數據,最后處理它。在異步編程中,一個函數請求數據之后,會在數據准備就緒后,讓外部庫調用其callback函數。
Deferred
Twisted使用Deferred
對象來管理callback序列。作為Twisted庫的“客戶端”,應用程序將一連串函數添加到Deferred對象中,當異步請求的結果准備就緒時,這一連串函數將被按順序調用(這一連串函數被稱為一個callback序列,或是一條callback鏈),一起添加的還有另外一連串函數,當異步請求出現錯誤的時候,他們將被調用(稱作一個errback序列,或是一條errback鏈)。異步庫代碼會在結果准備就緒時,調用第一個callback,或是在出現錯誤時,調用第一個errback,然后Deferred
對象就會將callback或errback的返回結果傳遞給鏈中的下一個函數。
Deferred解決的問題
Deferred被設計用來幫助解決第二類並發問題——非計算集中型任務,並且延遲時間是可以估計的。等待硬盤訪問,數據庫訪問和網絡訪問的函數都屬於這一類,盡管時間延遲不盡相同。
Deferred被設計用來使Twisted程序可以無阻塞地等待數據,直到數據准備就緒。為了達到這個目的,Deferred為庫與應用程序提供了一個簡單的callback管理接口。庫可以通過調用Deferred.callback
來把准備就緒的數據傳回給應用程序,或者調用Deferred.errback
來報告一個錯誤。應用程序可以按它們希望的順序,設置結果處理邏輯,以callback與errback的形式添加到Deferred對象中去。
使 CPU盡可能的有效率,是Deferred與該問題的其他解決方案背后的基本思路。如果一個任務在等待數據,與其讓CPU(以及程序本身!)眼巴巴地等着數據(對於進程,這通常叫做“阻塞”),不如讓程序同時執行些別的操作,並且同時留意着某些信號——一旦數據准備就緒,程序就可以(但不是立即——譯者注)回到剛才的地方繼續執行。
在Twisted里,一個函數返回一個Deferred對象,意味着它給調用者一個信號:它在等待數據。當數據准備就緒后,程序就會激活那個Deferred對象的callback鏈,來處理數據。
Deferred——數據即將到來的信號
在之前我們發送電子郵件的例子中,父函數調用了一個連接遠程服務器的函數。異步性要求這個連接函數必須不等待結果而直接返回,父函數才可以做其他事情。可是,父函數或者它的控制程序又是怎么知道連接尚未建立的呢?當連接建立后,它又是怎么使用連接的呢?
Twisted有一個對象可以作為這種情況的一個信號。連接函數返回一個twisted.internet.defer.Deferred
對象,給出一個操作尚未完成的信號。
Deferred有兩個目的。第一,它說“我是一個信號,只是通知你不管你剛才要我做的什么,結果還沒有出來”。第二,你可以讓Deferred在結果出來后執行你的東西。
callback
添加一個callback——這就是你讓Deferred在結果出來后執行你東西的辦法,也就是讓Deferred在結果出來后調用一個方法。
有一個Twisted庫函數返回Deferred,就是twisted.web.client.getPage
(這是一個異步獲取網頁的函數。——譯者注)。在這個例子里,我們調用了getPage
——它返回了一個Deferred,然后添加了一個callback來處理返回的頁面內容——當然處理是發生在數據准備就緒之后:
from twisted.web.client import getPage
from twisted.internet import reactor
def printContents(contents):
'''
這就是“callback”函數,被添加到Deferred,當“承諾了一定會有的數據”准備就緒后,
Deffered會調用它
'''
print "Deferred調用了printContents,內容如下:"
print contents
# 停止Twisted事件處理系統————這通常有更高層的辦法
reactor.stop()
# 調用getPage,它會馬上返回一個Deferred————承諾一旦頁面內容下載完了,
# 就會把他們傳給我們的callback們
deferred = getPage('http://twistedmatrix.com/') //一旦調用getPage下載頁面完成,則立即調用callback對應的函數.即addCallback(printContents)
中的printContents函數.
# 給Deferred添加一個callback————要求它在頁面內容下載完后,調用printContents
deferred.addCallback(printContents)
# 啟動Twisted事件處理系統,同樣,這不是通常的辦法
reactor.run()
添加兩個callback是一種非常常見的Deferred用法。第一個callback的返回結果會傳給第二個callback:
from twisted.web.client import getPage
from twisted.internet import reactor
def lowerCaseContents(contents):
'''
這是一個“callback”函數,被添加到Deferred,當“承諾了一定會有的數據”准備就緒后,
Deffered會調用它。它把所有的數據變成小寫
'''
return contents.lower()
def printContents(contents):
'''
這是一個“callback”函數,在lowerCaseContents之后被添加到Deferred,
Deferred會把lowerCaseContents的返回結果作為參數,調用這個callback
'''
print contents
reactor.stop()
deferred = getPage('http://twistedmatrix.com/')
# 向Deferred中添加兩個callback————讓Deferred在頁面內容下載完之后執行
# lowerCaseContents,然后將其返回結果作為參數,調用printContents
deferred.addCallback(lowerCaseContents) //當有多個addCallback的時候,第一個addCallback(lowerCaseContents)執行后的結果,會當作第二個addCallback(printContents)
函數的參數
deferred.addCallback(printContents)
reactor.run()
錯誤處理:errback
正如異步函數會在其結果產生之前返回,在有可能檢測到錯誤之前返回也是可以的:失敗的連接,錯誤的數據,協議錯誤,等等。正如你可以將callback添加到Deferred,你也可以將錯誤處理邏輯(“errback”)添加到Deferred,當出現錯誤,數據不能正常取回時,Deferred會調用它:
from twisted.web.client import getPage
from twisted.internet import reactor
def errorHandler(error):
'''
這是一個“errback”函數,被添加到Deferred,當出現錯誤事件是,Deferred將會調用它
'''
# 這么處理錯誤並不是很實際,我們只是把它打出來:
print "An error has occurred: <%s>" % str(error)
# 然后我們停止整個處理過程:
reactor.stop()
def printContents(contents):
'''
這是一個“callback”函數,被添加到Deferred,Deferred會把頁面內容作為參數調用它
'''
print contents
reactor.stop()
# 我們請求一個不存在的頁面,來演示錯誤鏈
deferred = getPage('http://twistedmatrix.com/does-not-exist')
# 向Deferred添加callback,以處理頁面內容
deferred.addCallback(printContents)
# 向Deferred添加errback,以處理任何錯誤
deferred.addErrback(errorHandler)
reactor.run()
結論
在這篇文檔中,你應該:
- 認識了為什么復雜網絡程序需要某種形式的並發性;
- 了解了Twisted框架用異步調用的形式支持並發;
- 了解了Twisted框架使用Deferred對象來管理callback鏈;
- 認識了
getPage
函數如何返回一個Deferred對象; - 向那個Deferred對象添加了callback與errback;以及
- 認識了Deferred的callback鏈與errback鏈的觸發。
參考資料
因為Deferred的抽象是Twisted編程中如此核心的一部分,以至於另外還有幾篇關於它的詳細的指南:
- 使用Deferred,一篇關於使用Deferred的更完整的指南,包括鏈式Deferred。
- 產生Deferred,一篇關於創建Deferred和觸發他們callback鏈的指南。