1 介紹
1.1 為什麽要使用Stackless
摘自?stackless?網站。
Note
Stackless Python 是Python編程語言的壹個增強版本,它使程序員從基於線程的編程方式中獲得好處,並避免傳統線程所帶來的性能與復雜度問題。Stackless為 Python帶來的微線程擴展,是壹種低開銷、輕量級的便利工具,如果使用得當,可以獲益如下:
改進程序結構
增進代碼可讀性
提高編程人員生產力
以上是Stackless Python很簡明的釋義,但其對我們意義何在?——就在於Stackless提供的並發建模工具,比目前其它大多數傳統編程語言所提供的,都更加易用: 不僅是Python自身,也包括Java、C++,以及其它。盡管還有其他壹些語言提供並發特性,可它們要麽是主要用於學術研究的(如 Mozart/Oz),要麽是罕為使用、或用於特殊目的的專業語言(如Erlang)。而使用stackless,妳將會在Python本身的所有優勢之 上,在壹個(但願)妳已經很熟悉的環境中,再獲得並發的特性。
這自然引出了個問題:為什麽要並發?
1.1.1 現實世界就是並發的
現實世界就是“並發”的,它是由壹群事物(或“演員”)所組成,而這些事物以壹種對彼此所知有限的、松散耦合的方式相互作用。傳說中面向對象編程有 壹個好處,就是對象能夠對現實的世界進行模擬。這在壹定程度上是正確的,面向對象編程很好地模擬了對象個體,但對於這些對象個體之間的交互,卻無法以壹種 理想的方式來表現。例如,如下代碼實例,有什麽問題?
def familyTacoNight():husband.eat(dinner)
wife.eat(dinner)
son.eat(dinner)
daughter.eat(dinner)
第壹印象,沒問題。但是,上例中存在壹個微妙的安排:所有事件是次序發生的,即:直到丈夫吃完飯,妻子才開始吃;兒子則壹直等到母親吃完才吃;而女 兒則是最後壹個。在現實世界中,哪怕是丈夫還堵車在路上,妻子、兒子和女兒仍然可以該吃就吃,而要在上例中的話,他們只能餓死了——甚至更糟:永遠沒有人 會知道這件事,因為他們永遠不會有機會拋出壹個異常來通知這個世界!
1.1.2 並發可能是(僅僅可能是)下壹個重要的編程範式
我個人相信,並發將是軟件世界裏的下壹個重要範式。隨著程序變得更加復雜和耗費資源,我們已經不能指望摩爾定律來每年給我們提供更快的CPU了,當 前,日常使用的個人計算機的性能提升來自於多核與多CPU機。壹旦單個CPU的性能達到極限,軟件開發者們將不得不轉向分布式模型,靠多臺計算機的互相協 作來建立強大的應用(想想GooglePlex)。為了取得多核機和分布式編程的優勢,並發將很快成為做事情的方式的事實標準。
1.2 安裝stackless
安裝Stackless的細節可以在其網站上找到。現在Linux用戶可以通過SubVersion取得源代碼並編譯;而對於Windows用戶, 則有壹個.zip文件供使用,需要將其解壓到現有的Python安裝目錄中。接下來,本教程假設Stackless Python已經安裝好了,可以工作,並且假設妳對Python語言本身有基本的了解。
2 stackless起步
本章簡要介紹了?stackless?的基本概念,後面章節將基於這些基礎,來展示更加實用的功能。
2.1 微進程(tasklet)
微進程是stackless的基本構成單元,妳可以通過提供任壹個Python可調用對象(通常為函數或類的方法)來建立它,這將建立壹個微進程並將其添加到調度器。這是壹個快速演示:
Python 2.4.3 Stackless 3.1b3 060504 (#69, May ?3 2006, 19:20:41) [MSC v.1310 32bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import stackless
>>>
>>> def print_x(x):
... print x
...
>>> stackless.tasklet(print_x)('one')
<stackless.tasklet object at 0x00A45870>
>>> stackless.tasklet(print_x)('two')
<stackless.tasklet object at 0x00A45A30>
>>> stackless.tasklet(print_x)('three')
<stackless.tasklet object at 0x00A45AB0>
>>>
>>> stackless.run()
one
two
three
>>>
註意,微進程將排起隊來,並不運行,直到調用?stackless.run()?。
2.2 調度器(scheduler)
調度器控制各個微進程運行的順序。如果剛剛建立了壹組微進程,它們將按照建立的順序來執行。在現實中,壹般會建立壹組可以再次被調度的微進程,好讓每個都有輪次機會。壹個快速演示:
Python 2.4.3 Stackless 3.1b3 060504 (#69, May ?3 2006, 19:20:41) [MSC v.1310 32bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import stackless
>>>
>>> def print_three_times(x):
... print "1:", x
... stackless.schedule()
... print "2:", x
... stackless.schedule()
... print "3:", x
... stackless.schedule()
...
>>>
>>> stackless.tasklet(print_three_times)('first')
<stackless.tasklet object at 0x00A45870>
>>> stackless.tasklet(print_three_times)('second')
<stackless.tasklet object at 0x00A45A30>
>>> stackless.tasklet(print_three_times)('third')
<stackless.tasklet object at 0x00A45AB0>
>>>
>>> stackless.run()
1: first
1: second
1: third
2: first
2: second
2: third
3: first
3: second
3: third
>>>
註意:當調用?stackless.schedule()?的時候,當前活動微進程將暫停執行,並將自身重新插入到調度器隊列的末尾,好讓下壹個微進程被執行。壹旦在它前面的所有其他微進程都運行過了,它將從上次 停止的地方繼續開始運行。這個過程會持續,直到所有的活動微進程都完成了運行過程。這就是使用stackless達到合作式多任務的方式。
2.3 通道(channel)
通道使得微進程之間的信息傳遞成為可能。它做到了兩件事:
能夠在微進程之間交換信息。
能夠控制運行的流程。
又壹個快速演示:
C:>c:python24pythonPython 2.4.3 Stackless 3.1b3 060504 (#69, May ?3 2006, 19:20:41) [MSC v.1310 32
bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import stackless
>>>
>>> channel = stackless.channel()
>>>
>>> def receiving_tasklet():
... print "Recieving tasklet started"
... print channel.receive()
... print "Receiving tasklet finished"
...
>>> def sending_tasklet():
... print "Sending tasklet started"
... channel.send("send from sending_tasklet")
... print "sending tasklet finished"
...
>>> def another_tasklet():
... print "Just another tasklet in the scheduler"
...
>>> stackless.tasklet(receiving_tasklet)()
<stackless.tasklet object at 0x00A45B30>
>>> stackless.tasklet(sending_tasklet)()
<stackless.tasklet object at 0x00A45B70>
>>> stackless.tasklet(another_tasklet)()
<stackless.tasklet object at 0x00A45BF0>
>>>
>>> stackless.run()
Recieving tasklet started
Sending tasklet started
send from sending_tasklet
Receiving tasklet finished
Just another tasklet in the scheduler
sending tasklet finished
>>>
接收的微進程調用?channel.receive()?的時候,便阻塞住,這意味著該微進程暫停執行,直到有信息從這個通道送過來。除了往這個通道發送信息以外,沒有其他任何方式可以讓這個微進程恢復運行。
若有其他微進程向這個通道發送了信息,則不管當前的調度到了哪裏,這個接收的微進程都立即恢復執行;而發送信息的微進程則被轉移到調度列表的末尾,就像調用了?stackless.schedule()?壹樣。
同樣註意,發送信息的時候,若當時沒有微進程正在這個通道上接收,也會使當前微進程阻塞:
>>> stackless.tasklet(sending_tasklet)()<stackless.tasklet object at 0x00A45B70>
>>> stackless.tasklet(another_tasklet)()
<stackless.tasklet object at 0x00A45BF0>
>>>
>>> stackless.run()
Sending tasklet started
Just another tasklet in the scheduler
>>>
>>> stackless.tasklet(another_tasklet)()
<stackless.tasklet object at 0x00A45B30>
>>> stackless.run()
Just another tasklet in the scheduler
>>>
>>> #Finally adding the receiving tasklet
...
>>> stackless.tasklet(receiving_tasklet)()
<stackless.tasklet object at 0x00A45BF0>
>>>
>>> stackless.run()
Recieving tasklet started
send from sending_tasklet
Receiving tasklet finished
sending tasklet finished
發送信息的微進程,只有在成功地將數據發送到了另壹個微進程之後,才會重新被插入到調度器中。
2.4 總結
以上涵蓋了stackless的大部分功能。似乎不多是吧?——我們只使用了少許對象,和大約四五個函數調用,來進行操作。但是,使用這種簡單的API作為基本建造單元,我們可以開始做壹些真正有趣的事情。
3 協程(coroutine)
3.1 子例程的問題
大多數傳統編程語言具有子例程的概念。壹個子例程被另壹個例程(可能還是其它某個例程的子例程)所調用,或返回壹個結果,或不返回結果。從定義上說,壹個子例程是從屬於其調用者的。
見下例:
def ping():print "PING"
pong()
def pong():
print "PONG"
ping()
ping()
有經驗的編程者會看到這個程序的問題所在:它導致了堆棧溢出。如果運行這個程序,它將顯示壹大堆討厭的跟蹤信息,來指出堆棧空間已經耗盡。
3.1.1 堆棧
我仔細考慮了,自己對C語言堆棧的細節究竟了解多少,最終還是決定完全不去講它。似乎,其他人對其所嘗試的描述,以及圖表,只有本身已經理解了的人才能看得懂。我將試著給出壹個最簡單的說明,而對其有更多興趣的讀者可以從網上查找更多信息。
每當壹個子例程被調用,都有壹個“棧幀”被建立,這是用來保存變量,以及其他子例程局部信息的區域。於是,當妳調用 ping() ,則有壹個棧幀被建立,來保存這次調用相關的信息。簡言之,這個幀記載著 ping 被調用了。當再調用 pong() ,則又建立了壹個棧幀,記載著 pong 也被調用了。這些棧幀是串聯在壹起的,每個子例程調用都是其中的壹環。就這樣,堆棧中顯示: ping 被調用所以 pong 接下來被調用。顯然,當 pong() 再調用 ping() ,則使堆棧再擴展。下面是個直觀的表示:
幀 堆棧
1 ping 被調用
2 ping 被調用,所以 pong 被調用
3 ping 被調用,所以 pong 被調用,所以 ping 被調用
4 ping 被調用,所以 pong 被調用,所以 ping 被調用,所以 pong 被調用
5 ping 被調用,所以 pong 被調用,所以 ping 被調用,所以 pong 被調用,所以 ping 被調用
6 ping 被調用,所以 pong 被調用,所以 ping 被調用,所以 pong 被調用,所以 ping 被調用……
現在假設,這個頁面的寬度就表示系統為堆棧所分配的全部內存空間,當其頂到頁面的邊緣的時候,將會發生溢出,系統內存耗盡,即術語“堆棧溢出”。
3.1.2 那麽,為什麽要使用堆棧?
上例是有意設計的,用來體現堆棧的問題所在。在大多數情況下,當每個子例程返回的時候,其棧幀將被清除掉,就是說堆棧將會自行實現清理過程。這壹般 來說是件好事,在C語言中,堆棧就是壹個不需要編程者來手動進行內存管理的區域。很幸運,Python程序員也不需要直接來擔心內存管理與堆棧。但是由於 Python解釋器本身也是用C實現的,那些實現者們可是需要擔心這個的。使用堆棧是會使事情方便,除非我們開始調用那種從不返回的函數,如上例中的,那 時候,堆棧的表現就開始和程序員別扭起來,並耗盡可用的內存。
3.2 走進協程
此時,將堆棧弄溢出是有點愚蠢的。 ping() 和 pong() 本不是真正意義的子例程,因為其中哪個也不從屬於另壹個,它們是“協程”,處於同等的地位,並可以彼此間進行無縫通信。
幀 堆棧
1 ping 被調用
2 pong 被調用
3 ping 被調用
4 pong 被調用
5 ping 被調用
6 pong 被調用
在stackless中,我們使用通道來建立協程。還記得嗎,通道所帶來的兩個好處中的壹個,就是能夠控制微進程之間運行的流程。使用通道,我們可以在 ping 和 pong 這兩個協程之間自由來回,要多少次就多少次,都不會堆棧溢出:
## pingpong_stackless.py
#
import stackless
ping_channel = stackless.channel()
pong_channel = stackless.channel()
def ping():
while ping_channel.receive(): #在此阻塞
print "PING"
pong_channel.send("from ping")
def pong():
while pong_channel.receive():
print "PONG"
ping_channel.send("from pong")
stackless.tasklet(ping)()
stackless.tasklet(pong)()
# 我們需要發送壹個消息來初始化這個遊戲的狀態
# 否則,兩個微進程都會阻塞
stackless.tasklet(ping_channel.send)('startup')
stackless.run()
妳可以運行這個程序要多久有多久,它都不會崩潰,且如果妳檢查其內存使用量(使用Windows的任務管理器或Linux的top命令),將會發現 使用量是恒定的。這個程序的協程版本,不管運行壹分鐘還是壹天,使用的內存都是壹樣的。而如果妳檢查原先那個遞歸版本的內存用量,則會發現其迅速增長,直 到崩潰。
3.3 總結
是否還記得,先前我提到過,那個代碼的遞歸版本,有經驗的程序員會壹眼看出毛病。但老實說,這裏面並沒有什麽“計算機科學”方面的原因在阻礙它的正 常工作,有些讓人堅信的東西,其實只是個與實現細節有關的小問題——只因為大多數傳統編程語言都使用堆棧。某種意義上說,有經驗的程序員都是被洗了腦,從 而相信這是個可以接受的問題。而stackless,則真正察覺了這個問題,並除掉了它。
4 輕量級線程
與當今的操作系統中內建的、和標準Python代碼中所支持的普通線程相比,“微線程”要更為輕量級,正如其名稱所暗示。它比傳統線程占用更少的內存,並且微線程之間的切換,要比傳統線程之間的切換更加節省資源。
為了準確說明微線程的效率究竟比傳統線程高多少,我們用兩者來寫同壹個程序。
4.1 hackysack模擬
Hackysack是壹種遊戲,就是壹夥臟乎乎的小子圍成壹個圈,來回踢壹個裝滿了豆粒的沙包,目標是不讓這個沙包落地,當傳球給別人的時候,可以耍各種把戲。踢沙包只可以用腳。
在我們的簡易模擬中,我們假設壹旦遊戲開始,圈裏人數就是恒定的,並且每個人都是如此厲害,以至於如果允許的話,這個遊戲可以永遠停不下來。
4.2 遊戲的傳統線程版本
import threadimport random
import sys
import Queue
class hackysacker:
counter = 0
def __init__(self,name,circle):
self.name = name
self.circle = circle
circle.append(self)
self.messageQueue = Queue.Queue()
thread.start_new_thread(self.messageLoop,())
def incrementCounter(self):
hackysacker.counter += 1
if hackysacker.counter >= turns:
while self.circle:
hs = self.circle.pop()
if hs is not self:
hs.messageQueue.put('exit')
sys.exit()
def messageLoop(self):
while 1:
message = self.messageQueue.get()
if message == "exit":
debugPrint("%s is going home" % self.name)
sys.exit()
debugPrint("%s got hackeysack from %s" % (self.name, message.name))
kickTo = self.circle[random.randint(0,len(self.circle)-1)]
debugPrint("%s kicking hackeysack to %s" % (self.name, kickTo.name))
self.incrementCounter()
kickTo.messageQueue.put(self)
def debugPrint(x):
if debug:
print x
debug=1
hackysackers=5
turns = 5