作者:dave@http://krondo.com/our-eye-beams-begin-to-twist/ 譯者:楊曉偉(采用意譯)
可以從這里從頭開始閱讀這個系列。
用twisted的方式實現前面的內容
最終我們將使用twisted的方式來重新實現我們前面的異步模式客戶端。不過,首先我們先稍微寫點簡單的twisted程序來認識一下twisted。
最最簡單的twisted程序就是下面的代碼,其在twisted-intro目錄中的basic-twisted/simple.py中。
from twisted.internet import reactor
reactor.run()
可以用下面的命令來運行它:
python basic-twisted/simple.py
正如在第二部分所說的那樣,twisted是實現了Reactor模式的,因此它必然會有一個對象來代表這個reactor或者說是事件循環,而這正是twisted的核心。上面代碼的第一行引入了reactor,第二行開始啟動事件循環。
這個程序什么事情也不做。除非你通過ctrl+c來終止它,否則它會一直運行下去。正常情況下,我們需要給出事件循環或者文件描述符來監視I/O(連接到某個服務器上,比如說我們那個詩歌服務器)。后面我們會來介紹這部分內容,現在這里的reactor被卡住了。值得注意的是,這里並不是一個在不停運行的簡單循環。如果你在桌面上有個CPU性能查看器,可以發現這個循環體不會帶來任何性能損失。實際上,這個reactor被卡住在第二部分圖5的最頂端,等待永遠不會到來的事件發生(更具體點說是一個調用select函數,卻沒有監視任何文件描述符)。
下面我們會讓這個程序豐富起來,不過事先要說幾個結論:
- Twisted的reactor只有通過調用reactor.run()來啟動。
- reactor循環是在其開始的進程中運行,也就是運行在主進程中。
- 一旦啟動,就會一直運行下去。reactor就會在程序的控制下(或者具體在一個啟動它的線程的控制下)。
- reactor循環並不會消耗任何CPU的資源。
- 並不需要顯式的創建reactor,只需要引入就OK了。
最后一條需要解釋清楚。在Twisted中,reactor是Singleton(也是一種模式),即在一個程序中只能有一個reactor,並且只要你引入它就相應地創建一個。上面引入的方式這是twisted默認使用的方法,當然了,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
以避免安裝默認
reactor
,而是在你要使用
reactor
的區域內安裝。
下面
是使用
pollreactor
重寫上上面的程序,可以在
basic-twisted/simple-poll.py文件中找到中找到:
from twited.internet import pollreactor pollreactor.install() from twisted.internet import reactor reactor.run()
上面這段代碼同樣沒有做任何事情。
后面我們都會只使用默認的
reactor
,就單純為了學習來說 ,所有的不同的
reactor
做的事情都一樣。
你好,
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
函數,並傳給它一個我們想調用函數的引用來實現
hello
函數的調用。當然,我們必須在啟動
reactor
之前完成這些工作。
我們使用回調來描述
hello
函數的引用。回調
實際上就是交給
Twisted
(或者其它框架)的一個函數引用,這樣
Twisted
會在合適的時間調用這個函數引用指向的函數,具體到這個程序中,是在
reactor
啟動的時候調用。由於
Twisted
循環是獨立於我們的代碼,我們的業務代碼與
reactor
核心代碼的絕大多數交互都是通過使用
Twisted
的
APIs
回調我們的業務函數來實現的。
我們可以通過下面這段代碼來觀察
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() <-- This is where we called the reactor ... ... <-- A bunch of Twisted function calls ... traceback.print_stack() <-- The second line in the stack function
不用考慮這其中的若干
Twisted
本身的函數。只需要關心
reactor.run()
與我們自己的函數調用之間的關系即可。
有關回調的一些其它說明:
Twisted
並不是唯一使用回調的框架。許多歷史悠久的框架都已在使用它。諸多
GUI
的框架也是基於回調來實現的,如
GTK
和
QT
。
交互式程序的編程人員特別喜歡回調。也許喜歡到想嫁給它。也許已經這樣做了。但下面這幾點值得我們仔細考慮下:
reactor
模式是單線程的。
像
Twisted
這種交互式模型已經實現了
reactor
循環,意味無需我們親自去實現它。
我們仍然需要框架來調用我們自己的代碼來完成業務邏輯。
因為在單線程中運行,要想跑我們自己的代碼,必須在
reactor
循環中調用它們。
reactor
事先並不知道調用我們代碼的哪個函數
這樣的話,回調並不僅僅是一個可選項,而是游戲規則的一部分。
圖
6
說明了回調過程中發生的一切:
圖
6 reactor
啟用回調
圖
6
揭示了回調中的幾個重要特性:
- 我們的代碼與Twisted代碼運行在同一個進程中。
- 當我們的代碼運行時,Twisted代碼是處於暫停狀態的。
- 同樣,當Twisted代碼處於運行狀態時,我們的代碼處於暫停狀態。
- reactor事件循環會在我們的回調函數返回后恢復運行。
在一個回調函數執行過程中,實際上
Twisted
的循環
是
被有效地阻塞在我們的代碼上
的
。因此,因此我們應該確保回調函數不要浪費時間(盡快返回)。特別需要強調
的是,我們應該盡量避免在回調函數中使用會阻塞
I/O
的函數。否則,我們將失去所有使用
reactor
所帶來的優勢。
Twisted
是不會采取特殊的預防措施來防止我們使用可阻塞的代碼的,這需要我們自己來確保上面的情況不會發生。正如我們實際所看到的一樣,對於普通網絡
I/O
的例子,由於我們讓
Twisted
替我們完成了異步通信,因此我們無需擔心上面的事情發生。
其它也可能會產生阻塞的操作是讀或寫一個非
socket
文件描述符(如管道)或者是等待一個子進程完成。
如何從阻塞轉換到非阻塞操作取決你具體的操作是什么,但是也有一些
Twisted APIs
會幫助你實現轉換。值得注意的是,很多標准的
Python
方法沒有辦法轉換為非阻塞方式。例如,
os.system
中的很多方法會在子進程完成前一直處於阻塞狀態。這也就是它工作的方式。所以當你使用
Twisted
時,避
開
使用
os.system
。
退出
Twisted
原來我們可以使用
reactor
的
stop
方法來停止
Twisted
的
reactor
。但是一旦
reactor
停止就無法再啟動了。(
Dave
的意思是,停止就退出程序了),因此只有在你想退出程序時才執行這個操作。
下面是退出代碼,代碼文件是
basic-twisted/countdown.py:
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
函數為
Twisted
注冊了一個回調函數。
callLater
中的第二個參數是回調函數,第一個則是說明你希望在將來幾秒鍾時執行你的回調函數。那
Twisted
如何來在指定的時間執行我們安排好的的回調函數。由於程序並沒有監聽任何文件描述符,為什么它沒有像前那些程序那樣卡在
select
循環上?
select
函數,或者其它類似的函數,同樣會接納一個超時參數。如果在只提供一個超時參數值並且沒有可供
I/O
操作的文件描述符而超時時間到時,
select
函數同樣會返回。因此,如果設置一個
0
的超時參數,那么會無任何阻塞地立即檢查所有的文件描述符集。
你可以將超時作為圖
5
中循環等待中的一種事件來看待。並且
Twisted
使用超時事件來確保那些通過
callLater
函數注冊的延時回調在指定的時間執行。或者更確切的說,在指定時間的前后會執行。如果一個回調函數執行時間過長,那么下面的延時回調函數可能會被相應的后延執行。
Twisted
的
callLater
機制並不為硬實時系統提供任何時間上的保證。
下面是上面程序的輸出:
Start! 5 ... 4 ... 3 ... 2 ... 1 ... Stop!
捕獲
它,
Twisted
由於
Twisted
經常會在回調中結束調用我們的代碼,因此你可能會想,如果我們的回調函數中出現異常會發生什么狀況。(
Dave
的意思是說,在結束我們的回調函數后會再次回到
Twisted
代碼中,若在我們的回調中發生異常,那是不是異常會跑到
Twisted
代碼中,而造成不可想象的后果 )讓我們來試試,在
basic-twisted/exception.py中的程序會在一個回調函數中引發一個異常,但是這不會影響下一個回調:
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.
注意
,盡管我們看到了因第一個回調函數引發異常而出現的跟蹤棧,第二個回調函數依然能夠執行。如果你將
reactor.stop()
注釋掉的話,程序會繼續運行下去。所以說,
reactor
並不會因為回調函數中出現失敗(雖然它會報告異常)而停止運行。
網絡服務器通常需要這種健壯的軟件。它們通常不希望由於一個隨機的
Bug
導致崩潰。
也並不是說當我們發現自己的程序內部有問題時,就垂頭喪氣。只是想說
Twisted
能夠很好的從失敗的回調中返回並繼續執行。
請繼續講解詩歌服務器
現在,我們已經准備好利用
Twisted
來搭建我們的詩歌服務器。在第
4
部分,我們會實現我們的異步模式的詩歌服務器的
Twisted
版。