[Twisted] Part 3: Our Eye-beams Begin to Twist
本文由Dave的Part 3: Our Eye-beams Begin to Twist翻譯而成,你可以由Part 1開始閱讀這個系列的文章,也可以在這裡找到整個系列的目錄。
下面列出了絕對是最簡單的twisted程式,這個程式在twisted-intro目錄中的basic-twisted/simple.py中:
你可以像這樣執行它:
正如我們在Part 2所見,Twisted是reactor模式的實現,因此它必然包含了一個代表reactor或事件迴圈的物件,而這正是所有Twisted程式的核心。我們程式的第一行引入了reactor物件,所以我們可以使用它,而第二行告訴reactor開始執行迴圈。
這個程式什麼事情也不做。你必須透過ctrl+c來終止它,否則它會永遠運行下去。通常對於我們想監視的I/O,我們會給迴圈一個或多個file descriptors (例如連接到詩歌伺服器)。後面我們會看到如何做到這件事,但是現在我們的reactor卡住了。注意這並不是一個不斷循環的忙碌迴圈(busy loop),如果你的螢幕上有個CPU監視器,你不會發現任何由無限迴圈引起的CPU使用率尖峰。事實上,我們的程式沒有使用任何的CPU,這個reactor被卡在圖5的迴圈頂端,等待著一個永遠不會發生的事件(具體來說,是等待一個沒有file descriptors的select呼叫)。
我們即將讓這個程式變得更有趣,但我們已經可以得出幾個結論:
最後一條需要解釋清楚。在Twisted中,reactor基本上是一個Singleton(單例模式)。只會有一個reactor,並且只要你引入它就會自動創建一個。如果你開啟twisted.internet套件中的reactor模組,你會看到一個非常小的程式,實際的實現方法在其他的檔案中(請參閱twisted.internet.selectreactor)。
Twisted實際上有多種reactor的實現方式,如Part 2所說,呼叫select指是等待file descriptors的其中一種方法。Twisted包含了幾種實現reactor的不同方法,例如twisted.internet.pollreactor使用poll代替select。
若要使用其它的reactor,需要在引入twisted.internet.reactor前安裝它。下面是安裝pollreactor的方法:
如果你沒有安裝其它特殊的reactor而引入了twisted.internet.reactor,那麼Twisted預設會為你安裝selectreactor,預設reactor的類型會因為你的作業系統與Twisted版本而有所不同。正因為如此,習慣性做法不要在最頂層的模組內引入reactor以避免安裝預設的reactor,而是在你要使用reactor的範圍內引入。
現在我們能使用pollreactor重寫上上面的程式,可以在basic-twisted/simple-poll.py中找到:
然後我們就有一個不做任何事情的poll迴圈,用來代替那個不做任何事情的select迴圈。
後面我們都會只使用預設的reactor,就單純為了學習來說,所有的reactor做的事情都一樣。
這程式在basic-twisted/hello.py中。如果你執行它,你會看到這樣的輸出:
你仍然需要自己關閉這個程式,因為印出這些訊息後程式會再次卡住。
注意hello函數是在reactor執行後被呼叫的。這意味著它是被reactor呼叫的,也就是說Twisted必須呼叫我們的函數。在這個程式中,我們藉由調用reactor的callWhenRunning方法,並給它一個我們要Twisted呼叫的函式的參考。當然,我們必須在啟動reactor之前完成這些工作。
我們使用callback(回呼、回調)這個術語來描述hello函數的引用。callback函式是我們給Twisted一個函式的參考,Twisted會在合適的時候「回撥電話(call us back)」給這個函式,在這個範例中是在reactor啟動之後。由於Twisted的迴圈與我們的程式碼是分開的,因此reactor的核心與我們程式的商業邏輯間的互動,都是由各種APIs提供給Twisted的callback函式開始。
我們可以通過下面這段代碼來觀察Twisted是如何呼叫我們程式碼:
你能在basic-twisted/stack.py找到這個程式,然後它會輸出像這樣的結果:
不用考慮這其中Twisted本身的函數。只需注意reactor.run()的呼叫與我們自己的callback函式之間的關係即可。
圖6說明了callback過程中發生的一切:
圖6顯示了callback的幾個重要特性:
在一個callback過程中,Twisted迴圈是有效地被我們的程式碼阻塞。因此我們應該確保我們的callback程式碼不要浪費任何時間,特別是我們應該避免在callback中呼叫會阻塞的I/O,否則我們會失去使用reactor所帶來的好處。Twisted不會對我們的程式碼採取任何特殊措施來防止阻塞,我們必須自己確保不會發生這樣的狀況。如我們實際使用上,在一般網路的I/O常見情況中,我們可以放心讓Twisted替我們執行非同步通訊。
其他潛在的阻塞作業的例子包括讀取或寫入一個非scoket的file descriptor(如pipe)或者等待一個子程序完成。如何從阻塞作業切換到非阻塞作業取決於你的作業正在做什麼,但通常會有一些Twisted API協助你切換。請注意,許多標準的Python函式無法切換到非阻塞模式,例如os.system函式會總是阻塞並等待子程序完成,這就是它工作的方式,所以在使用Twisted時你必須避開os.system,進而使用Twisted API來啟動子程序。
下面這段程式在basic-twisted/countdown.py中,它會在倒數五秒後停止reactor:
這個程式使用了callLater這個API對Twisted註冊了一個callback函式,在callLater中第一個引數是你希望callback函式執行的秒數,第二個引數是callback函式,你也可以使用浮點數來指定秒數。那麼Twisted如何安排在正確的時間執行callback?由於這個程式沒有監聽任何file descriptors,為何不會像之前的程式一樣卡在select迴圈呢?呼叫select或其他類似的函式時,會接收一個timeout的選項,如果給予了這個timeout值而且在指定的時間內沒有任何file descriptors準備好進行I/O,那麼select函式將會返回。順帶一提,如果將timeout設定為0,你可以沒有任何阻塞的快速檢查(或輪詢)一組file descriptors。
你可以將timeout視為圖5中迴圈等待中的另一種事件,並且Twisted使用timeout確保使用callLater註冊的「定時callback」會在正確的時間被呼叫,或者說在正確的時間前後。如果另一個callback執行的時間過長,則定時callback可能會延後執行。Twisted的callLater機制不能給予hard real-time(硬即時)系統需要的時間保證。
這邊是倒數程式的輸出:
當你在命令列中執行它時,你會看到這樣的輸出:
注意,儘管我們看到了因第一個callback拋出異常而出現的traceback,第二個callback依然能夠執行,如果你註解掉reactor.stop(),程式將會繼續執行下去。所以即使我們的callback失效,reactor也會繼續運作(儘管它會報告異常)。這並不是說當我們程式內部有問題時我們就垂頭喪氣,只是想說很高興知道Twisted在背後幫助我們(譯註: 不會因為callback失效而導致伺服器崩潰)。
Doing Nothing, the Twisted Way
終於,我們將使用Twisted重新實現我們非同步詩歌用戶端。但先讓我們嘗試寫一些非常簡單的Twisted程式。正如我在Part 2中所提到,我使用Twisted 8.2.0開發這些範例,Twisted API會發生變化,但我們將使用的核心API可能改變會比較緩慢,我希望這些範例可以在許多未來的版本中使用。如果你還沒安裝Twisted,你可以在這裡獲得。下面列出了絕對是最簡單的twisted程式,這個程式在twisted-intro目錄中的basic-twisted/simple.py中:
from twisted.internet import reactor
reactor.run()
你可以像這樣執行它:
python basic-twisted/simple.py
正如我們在Part 2所見,Twisted是reactor模式的實現,因此它必然包含了一個代表reactor或事件迴圈的物件,而這正是所有Twisted程式的核心。我們程式的第一行引入了reactor物件,所以我們可以使用它,而第二行告訴reactor開始執行迴圈。
這個程式什麼事情也不做。你必須透過ctrl+c來終止它,否則它會永遠運行下去。通常對於我們想監視的I/O,我們會給迴圈一個或多個file descriptors (例如連接到詩歌伺服器)。後面我們會看到如何做到這件事,但是現在我們的reactor卡住了。注意這並不是一個不斷循環的忙碌迴圈(busy loop),如果你的螢幕上有個CPU監視器,你不會發現任何由無限迴圈引起的CPU使用率尖峰。事實上,我們的程式沒有使用任何的CPU,這個reactor被卡在圖5的迴圈頂端,等待著一個永遠不會發生的事件(具體來說,是等待一個沒有file descriptors的select呼叫)。
我們即將讓這個程式變得更有趣,但我們已經可以得出幾個結論:
- Twisted的reactor迴圈只有通過呼叫reactor.run()來啟動。
- reactor迴圈是在其啟動的執行緒中運行,在這個案例中,它運行在主執行緒(也是唯一的執行緒)。
- 一旦迴圈啟動,就會一直運行下去。reactor就會在程式(或者在啟動它的執行緒)的控制下。
- 如果沒有任何事情要做,reactor迴圈並不會消耗任何CPU的資源。
- 並不需要明確的創建reactor,只需要引入。
最後一條需要解釋清楚。在Twisted中,reactor基本上是一個Singleton(單例模式)。只會有一個reactor,並且只要你引入它就會自動創建一個。如果你開啟twisted.internet套件中的reactor模組,你會看到一個非常小的程式,實際的實現方法在其他的檔案中(請參閱twisted.internet.selectreactor)。
Twisted實際上有多種reactor的實現方式,如Part 2所說,呼叫select指是等待file descriptors的其中一種方法。Twisted包含了幾種實現reactor的不同方法,例如twisted.internet.pollreactor使用poll代替select。
若要使用其它的reactor,需要在引入twisted.internet.reactor前安裝它。下面是安裝pollreactor的方法:
from twisted.internet import pollreactor
pollreactor.install()
如果你沒有安裝其它特殊的reactor而引入了twisted.internet.reactor,那麼Twisted預設會為你安裝selectreactor,預設reactor的類型會因為你的作業系統與Twisted版本而有所不同。正因為如此,習慣性做法不要在最頂層的模組內引入reactor以避免安裝預設的reactor,而是在你要使用reactor的範圍內引入。
注意:在撰寫本文時,Twisted已經朝向一種可以共存多個reactor的架構發展(譯註:目前版本的Twisted已經可以使用多個reactor),在這個架構下reactor物件將做為參考(reference)傳遞,而不是由模組引入。
注意:並非所有作業系統都支援呼叫poll,如果你的系統屬於這種狀況,這個範例將無法作用。
現在我們能使用pollreactor重寫上上面的程式,可以在basic-twisted/simple-poll.py中找到:
from twisted.internet import pollreactor
pollreactor.install()
from twisted.internet import reactor
reactor.run()
然後我們就有一個不做任何事情的poll迴圈,用來代替那個不做任何事情的select迴圈。
後面我們都會只使用預設的reactor,就單純為了學習來說,所有的reactor做的事情都一樣。
Hello, Twisted
讓我們寫個Twisted程式,然後讓它至少做些事情。這個程式在reactor迴圈開始後,會印出一條訊息在終端視窗中:def hello():
print 'Hello from the reactor loop!'
print 'Lately I feel like I\'m stuck in a rut.'
from twisted.internet import reactor
reactor.callWhenRunning(hello)
print 'Starting the reactor.'
reactor.run()
這程式在basic-twisted/hello.py中。如果你執行它,你會看到這樣的輸出:
Starting the reactor.
Hello from the reactor loop!
Lately I feel like I'm stuck in a rut.
你仍然需要自己關閉這個程式,因為印出這些訊息後程式會再次卡住。
注意hello函數是在reactor執行後被呼叫的。這意味著它是被reactor呼叫的,也就是說Twisted必須呼叫我們的函數。在這個程式中,我們藉由調用reactor的callWhenRunning方法,並給它一個我們要Twisted呼叫的函式的參考。當然,我們必須在啟動reactor之前完成這些工作。
我們使用callback(回呼、回調)這個術語來描述hello函數的引用。callback函式是我們給Twisted一個函式的參考,Twisted會在合適的時候「回撥電話(call us back)」給這個函式,在這個範例中是在reactor啟動之後。由於Twisted的迴圈與我們的程式碼是分開的,因此reactor的核心與我們程式的商業邏輯間的互動,都是由各種APIs提供給Twisted的callback函式開始。
我們可以通過下面這段代碼來觀察Twisted是如何呼叫我們程式碼:
import traceback
def stack():
print 'The python stack:'
traceback.print_stack()
from twisted.internet import reactor
reactor.callWhenRunning(stack)
reactor.run()
你能在basic-twisted/stack.py找到這個程式,然後它會輸出像這樣的結果:
The python stack:
...
reactor.run() <-- ...="" a="" bunch="" called="" calls="" code="" function="" in="" is="" line="" of="" reactor="" second="" stack="" the="" this="" traceback.print_stack="" twisted="" we="" where="">
不用考慮這其中Twisted本身的函數。只需注意reactor.run()的呼叫與我們自己的callback函式之間的關係即可。
What’s the deal with callbacks?
Twisted並不是唯一使用callback的框架。過去的非同步Python框架如Medusa與asyncore也使用它,GUI的toolkits如GTK與QT也是如此,許多GUI框架都是基於reactor迴圈。
回應式系統的開發人員確實喜歡callback,或許他們應該嫁給它,或許他們已經這樣做了。但考慮一下這個:
- reactor模式是單執行緒的。
- 像Twisted這種回應式框架實現了reactor迴圈,所以我們的程式不需要去實現它。
- 我們的程式仍然需要被呼叫來實現我們的商業邏輯。
- 因為reactor迴圈掌握了單執行緒,它將必須要呼叫我們的程式碼。
- reactor事先並不知道呼叫我們程式碼的哪個函數。
在這種狀況下,callbacks並不僅僅是一個選項-它是唯一的遊戲規則。
圖6說明了callback過程中發生的一切:
圖6顯示了callback的幾個重要特性:
- 我們的程式碼與Twisted程式碼運行在同一個執行緒中。
- 當我們的程式碼執行時,Twisted代碼是處於暫停狀態的。
- 同樣,當Twisted程式碼執行狀時,我們的程式碼處於暫停狀態。
- reactor迴圈會在我們的callback回傳後恢復。
在一個callback過程中,Twisted迴圈是有效地被我們的程式碼阻塞。因此我們應該確保我們的callback程式碼不要浪費任何時間,特別是我們應該避免在callback中呼叫會阻塞的I/O,否則我們會失去使用reactor所帶來的好處。Twisted不會對我們的程式碼採取任何特殊措施來防止阻塞,我們必須自己確保不會發生這樣的狀況。如我們實際使用上,在一般網路的I/O常見情況中,我們可以放心讓Twisted替我們執行非同步通訊。
其他潛在的阻塞作業的例子包括讀取或寫入一個非scoket的file descriptor(如pipe)或者等待一個子程序完成。如何從阻塞作業切換到非阻塞作業取決於你的作業正在做什麼,但通常會有一些Twisted API協助你切換。請注意,許多標準的Python函式無法切換到非阻塞模式,例如os.system函式會總是阻塞並等待子程序完成,這就是它工作的方式,所以在使用Twisted時你必須避開os.system,進而使用Twisted API來啟動子程序。
Goodbye, Twisted
我們可以使用reactor的stop方法來告訴Twisted的reactor停下來,但一旦停止了reactor就不能再重新開始(譯註: 如果在一個程式中呼叫兩次reactor.run()會在第二次執行時回傳一個twisted.internet.error.ReactorNotRestartable的錯誤),所以通常只有當你的程式要退出時才會執行。下面這段程式在basic-twisted/countdown.py中,它會在倒數五秒後停止reactor:
class Countdown(object):
counter = 5
def count(self):
if self.counter == 0:
reactor.stop()
else:
print self.counter, '...'
self.counter -= 1
reactor.callLater(1, self.count)
from twisted.internet import reactor
reactor.callWhenRunning(Countdown().count)
print 'Start!'
reactor.run()
print 'Stop!'
這個程式使用了callLater這個API對Twisted註冊了一個callback函式,在callLater中第一個引數是你希望callback函式執行的秒數,第二個引數是callback函式,你也可以使用浮點數來指定秒數。那麼Twisted如何安排在正確的時間執行callback?由於這個程式沒有監聽任何file descriptors,為何不會像之前的程式一樣卡在select迴圈呢?呼叫select或其他類似的函式時,會接收一個timeout的選項,如果給予了這個timeout值而且在指定的時間內沒有任何file descriptors準備好進行I/O,那麼select函式將會返回。順帶一提,如果將timeout設定為0,你可以沒有任何阻塞的快速檢查(或輪詢)一組file descriptors。
你可以將timeout視為圖5中迴圈等待中的另一種事件,並且Twisted使用timeout確保使用callLater註冊的「定時callback」會在正確的時間被呼叫,或者說在正確的時間前後。如果另一個callback執行的時間過長,則定時callback可能會延後執行。Twisted的callLater機制不能給予hard real-time(硬即時)系統需要的時間保證。
這邊是倒數程式的輸出:
Start!
5 ...
4 ...
3 ...
2 ...
1 ...
Stop!
Take That, Twisted
由於Twisted經常用callback的形式呼叫我們的程式碼,你可能會好奇當callback拋出異常時會發生什麼事情。讓我們來試試看,在basic-twisted/exception.py程式中有一個callback拋出異常,但另一個callback正常執行:def falldown():
raise Exception('I fall down.')
def upagain():
print 'But I get up again.'
reactor.stop()
from twisted.internet import reactor
reactor.callWhenRunning(falldown)
reactor.callWhenRunning(upagain)
print 'Starting the reactor.'
reactor.run()
當你在命令列中執行它時,你會看到這樣的輸出:
Starting the reactor.
Traceback (most recent call last):
... # I removed most of the traceback
exceptions.Exception: I fall down.
But I get up again.
注意,儘管我們看到了因第一個callback拋出異常而出現的traceback,第二個callback依然能夠執行,如果你註解掉reactor.stop(),程式將會繼續執行下去。所以即使我們的callback失效,reactor也會繼續運作(儘管它會報告異常)。這並不是說當我們程式內部有問題時我們就垂頭喪氣,只是想說很高興知道Twisted在背後幫助我們(譯註: 不會因為callback失效而導致伺服器崩潰)。
Poetry, Please
現在我們準備好用Twisted抓取一些詩歌,在Part 4我們會實作一個我們Twisted版本的非同步詩歌用戶端。Suggested Exercises
- 更新countdown.py程式,讓三個獨立執行的計數器以不同的速率執行。當所有計數器完成時停止reactor。
- 研究一下twisted.internet.task的LoopingCall類別。使用LoopingCall重寫上面的countdown程式。你只需要start與stop方法,並且絲毫不需要使用「deferred」這個回傳值。我們將在往後的章節學習到什麼是「deferred」值。
留言
張貼留言