在這個系列中,有一個事實我們還沒有介紹,即混合同步的"普通Python"代碼與異步Twisted代碼不是一個簡單的工作,因為在Twisted程序中阻滯不定時間將使異步模型的優(yōu)勢喪失殆盡.
如果你是初次接觸異步編程,那么你得到的知識看起來有一些局限.你可以在Twisted框架內(nèi)使用這些新技術(shù),而不是在更廣闊的一般Python代碼世界中.同時,當(dāng)用Twisted工作時,你僅僅局限于那些專門為了作為Twisted程序一部分所寫的庫,至少如果你想直接從 reactor
線程調(diào)用它們的時候.
但是異步編程技術(shù)已經(jīng)存在了很多年并且?guī)缀醪痪窒抻赥wisted.其實僅在Python中就有令人吃驚數(shù)量的異步編程模型. 搜索 一下就會看到很多. 它們在細(xì)節(jié)方面不同于Twisted,但是基本的思想(如異步I/O,將大規(guī)模數(shù)據(jù)流分割為小塊處理)是一樣的.所以如果你需要,或者選擇,使用一個不同的框架,你將會因為學(xué)習(xí)了Twisted而具備一個很好的開端.
當(dāng)我們移步Python之外,同樣會發(fā)現(xiàn)很多語言和系統(tǒng)基于或者使用異步編程模型.你在Twisted學(xué)習(xí)到的知識將繼續(xù)為你在異步編程方面開拓更廣闊的領(lǐng)域而服務(wù).
在這個部分,我們將簡單地看一看 Erlang,一種編程語言和運行時系統(tǒng),它以一種獨特的方式廣泛使用異步編程概念.請注意我們不是要開始寫 Erlang入門
.而是稍稍探索一下Erlang中包含的一些思想,看看這些與Twisted思想的聯(lián)系.基本主題就是你通過學(xué)習(xí)Twisted得到的知識可以應(yīng)用到學(xué)習(xí)其他技術(shù).
考慮 圖6 ,回調(diào)的圖形表示. 是 第六部分 中介紹的 詩歌代理3.0 的回調(diào)和 dataReceived 方法中的順序詩歌客戶端的原理. 每次從一個相連的詩歌服務(wù)器下載一小部分詩歌時將激發(fā)回調(diào).
假設(shè)我們的客戶端從3個不同的服務(wù)器下載3首詩.以 reactor
的角度看問題(這是在這個系列中一直主張的),我們得到一個單一的大循環(huán),當(dāng)每次輪詢時激發(fā)一個或多個回調(diào),如圖40:
![](images/p20_reactor-2.png 圖40 以 reactor 角度的回調(diào)
此圖顯示了 reactor
歡快地運轉(zhuǎn),每次詩歌到來時它調(diào)用 dataReceived
. 每次 dataReceived
調(diào)用應(yīng)用于一個特定的 PoetryProtocal
類實例. 我們知道一共有3個實例因為我們正在下載3首詩(所以必須有3個連接).
以一個Protocol實例的角度考慮這張圖.記住每個Protocol只有一個連接(一首詩). 這個實例可“看到”一個方法調(diào)用流,每個方法接收著詩歌的下一部分,如下:
dataReceived(self, "When I have fears")
dataReceived(self, " that I may cease to be")
dataReceived(self, "Before my pen has glea")
dataReceived(self, "n'd my teeming brain")
...
然而這不是嚴(yán)格意義上的Python循環(huán),我們可以將其概念化為一個循環(huán):
for data in poetry_stream(): # pseudo-code
dataReceived(data)
我們可以設(shè)想"回調(diào)循環(huán)",如圖41:
http://wiki.jikexueyuan.com/project/twisted-intro/images/p20_callback-loop.png" alt="" /> 圖41 一個虛擬回調(diào)循環(huán)
當(dāng)然這不是一個 for
循環(huán)或 while
循環(huán). 在我們詩歌客戶端中唯一重要的Python循環(huán)是 reactor
. 但是我們可以把每個Protocol視作一個虛擬循環(huán),當(dāng)有詩歌到來時它會啟動循環(huán). 根據(jù)這種想法, 我們可以用圖42重構(gòu)整個客戶端:
http://wiki.jikexueyuan.com/project/twisted-intro/images/p20_reactor-3.png" alt="" /> 圖42 reactor 轉(zhuǎn)動虛擬循環(huán)
在這張圖中,有一個大循環(huán) —— reactor
和三個虛擬循環(huán) —— 詩歌協(xié)議實例個體.大循環(huán)轉(zhuǎn)起來,如此,使得虛擬循環(huán)也轉(zhuǎn)起來了,就像一組環(huán)環(huán)相扣的齒輪.
Erlang,與Python一樣,源自一種八十年代創(chuàng)建的一般目的動態(tài)類型的編程語言.不像Python,Erlang是功能型的而不是面向?qū)ο蟮?并且在句法上類似懷舊的 Prolog, Erlang最初就是由其實現(xiàn)的. Erlang被設(shè)計為建立高度可靠的分布式電話系統(tǒng),因此Erlang包含廣泛的網(wǎng)絡(luò)支持.
Erlang的一個最獨特的特性是一個涉及輕量級進程的并發(fā)模型. 一個Erlang進程既不是一個操作系統(tǒng)進程也不是線程.它是在Erlang運行環(huán)境中一個獨立運行的函數(shù),它有自己的堆棧.Erlang進程不是輕量級的線程,因為Erlang進程不能共享狀態(tài)(許多數(shù)據(jù)類型也是不可變的,Erlang是一種函數(shù)式編程語言).一個Erlang進程可以與其他Erlang進程交互,但僅僅是通過發(fā)送消息,消息總是(至少概念上)被復(fù)制的而不是共享.
所以一個Erlang程序看起來如圖43:
http://wiki.jikexueyuan.com/project/twisted-intro/images/p20_erlang-11.png" alt="" /> 圖43 有3個進程的Erlang程序
在此圖中,個體進程變成了"真實的存在".因為進程在Erlang中是第一構(gòu)造,就像Python中的對象.但運行時變成了"虛擬的",不是由于它不存在,而是由于它不是一個簡單的循環(huán).Erlang運行時可能是多線程的,因為它必須去實現(xiàn)一個全面的編程語言,還要負(fù)責(zé)很多除異步I/O之外的東西.進一步,一個語言運行時也就是允許Erlang進程和代碼執(zhí)行的媒介,而不是像Twisted中的 reactor
那樣的額外構(gòu)造.
所以一個Erlang程序的更好表示如下圖44:
http://wiki.jikexueyuan.com/project/twisted-intro/images/p20_erlang-2.png" alt="" /> 圖44 有若干進程的Erlang程序
當(dāng)然, Erlang運行時確實需要使用異步I/O以及一個或多個選擇循環(huán),因為Erlang允許你創(chuàng)建 大量 進程. 大規(guī)模Erlang程序可以啟動成千上萬的Erlang進程,所以為每個進程分配一個實際地OS線程是問題所在.如果Erlang允許多進程執(zhí)行I/O,同時允許其他進程運行即便那個I/O阻塞了,那么異步I/O就必須被包含進來了.
注意我們關(guān)于Erlang程序的圖說明了每個進程是"靠它自己的力量"運行,而不是被回調(diào)旋轉(zhuǎn)著. 隨著 reactor
的工作被歸納成Erlang運行時的結(jié)構(gòu),回調(diào)不再扮演中心角色. 原來在Twisted中需要通過回調(diào)解決的問題,在Erlang中將通過從一個進程向另一個進程發(fā)送異步消息來解決.
讓我們看一下Erlang詩歌客戶端. 這次我們直接跳入工作版本而不是像在Twisted中慢慢地搭建它.同樣,這意味著不是完整版本的Erlang介紹. 但如果激起了你的興趣,我們在本部分最后推薦了一些深度閱讀資料.
Erlang客戶端位于 erlang-client-1/get-poetry. 為了運行它,你需要安裝 Erlang.
下面代碼是 main
函數(shù)代碼,與Python客戶端中的 main
函數(shù)具有相同的目的:
main([]) ->
usage();
main(Args) ->
Addresses = parse_args(Args),
Main = self(),
[erlang:spawn_monitor(fun () -> get_poetry(TaskNum, Addr, Main) end)
|| {TaskNum, Addr} <- enumerate(Addresses)],
collect_poems(length(Addresses), []).
如果你從來沒有見過Prolog或者相似的語言,那么Erlang的句法將顯得有一點奇怪.但是有一些人也這樣認(rèn)為Python.main
函數(shù)被兩個分離的句群定義,被分號分割. Erlang根據(jù)參數(shù)選擇運行哪一個句群,所以第一個句群只在我們執(zhí)行客戶端不提供任何命令行參數(shù)的情況下運行,它只打印出幫助信息.第二個句群是所有實際的處理.
Erlang函數(shù)中的每條語句以逗號分隔,函數(shù)以句號結(jié)尾.讓我們看一看第二個句群,第一行僅僅分析命令行參數(shù)并且將它們綁定到一個變量(Erlang中所有變量必須大寫).第二行使用 self
函數(shù)來獲取當(dāng)下正在運行的Erlang進程(而非OS進程)的ID.由于這是主函數(shù),你可以認(rèn)為它等價于Python中的 __main__
模塊. 第三行是最有趣的:
[erlang:spawn_monitor(fun () -> get_poetry(TaskNum, Addr, Main) end)
|| {TaskNum, Addr} <- enumerate(Addresses)],
這個語句是對Erlang列表的理解,與Python有相似的句法.它對每個需要連接的服務(wù)器產(chǎn)生新的Erlang進程. 同時每個進程將運行相同的 get_poetry
函數(shù), 但是根據(jù)特定的服務(wù)器用不同的參數(shù).我們同時傳遞主進程的PID以便新的進程可以把詩歌發(fā)送回來(你通常需要一個進程的PID來向它發(fā)送消息)
main
函數(shù)中的最后一條語句調(diào)用 collect_poems
函數(shù),它等待詩歌傳回來和 get_poetry
進程結(jié)束.我們可以看一下其他函數(shù),但首先你可能會對比一下Erlang的 main 函數(shù)與等價地Twisted客戶端中的 main 函數(shù).
現(xiàn)在讓我們看一下Erlang中的 get_poetry
函數(shù).事實上在我們的腳本中有兩個函數(shù)叫 get_poetry
.在Erlang中,一個函數(shù)被名字和元數(shù)同時確定,所以我們的腳本包含兩個不同的函數(shù), get_poetry/3
和 get_poetry/4
,它們分別接收3個或4個參數(shù).這里是 get_poetry/3,它是被 main
生成的:
get_poetry(Tasknum, Addr, Main) ->
{Host, Port} = Addr,
{ok, Socket} = gen_tcp:connect(Host, Port,
[binary, {active, false}, {packet, 0}]),
get_poetry(Tasknum, Socket, Main, []).
這個函數(shù)首先創(chuàng)建一個TCP連接,就像Twisted客戶端中的 get_poetry
.但之后,不是返回,而是繼續(xù)使用那個TCP連接,通過調(diào)用 get_poetry/4,如下:
get_poetry(Tasknum, Socket, Main, Packets) ->
case gen_tcp:recv(Socket, 0) of
{ok, Packet} ->
io:format("Task ~w: got ~w bytes of poetry from ~s\n",
[Tasknum, size(Packet), peername(Socket)]),
get_poetry(Tasknum, Socket, Main, [Packet|Packets]);
{error, _} ->
Main ! {poem, list_to_binary(lists:reverse(Packets))}
end.
這個Erlang函數(shù)正在做Twisted客戶端中 PoetryProtocol
的工作,不同的是它使用阻塞函數(shù)調(diào)用. gen_tcp:recv
函數(shù)等待在套接字上一些數(shù)據(jù)的到來(或者套接字關(guān)閉),無論要等多長時間.但Erlang中的"阻塞"函數(shù)僅阻塞正在運行函數(shù)的進程,而不是整個Erlang運行時.那個TCP套接字并不是一個真正的阻塞套接字(你不能在純Erlang代碼中創(chuàng)建一個真正的阻塞套接字).對于Erlang中的每個套接字,在運行時的某處,一個"真正的"TCP套接字被設(shè)置為非阻塞模型并且用作選擇循環(huán)的一部分.
但是Erlang進程并不知道這些.它僅僅等待一些數(shù)據(jù)的到來,如果阻塞了,其他Erlang進程會代替運行.甚至一個進程從不阻塞,Erlang運行時可以在任何時刻自由地在進程間切換.換句話說,Erlang具有一個非協(xié)同并發(fā)機制.
注意 get_poetry/4
,在收到一小部分詩歌后,繼續(xù)遞歸地調(diào)用它自己.對于一個急迫的語言程序員這看起來像耗盡內(nèi)存的良方,但Erlang編譯器卻可以優(yōu)化"尾"調(diào)用(函數(shù)調(diào)用一個函數(shù)中的最后一條語句)為循環(huán).這照亮了又一個有趣的Erlang客戶端和Twisted客戶端之間的平行對比.在Twisted客戶端中,"虛擬"循環(huán)是被 reaactor
創(chuàng)建的,它一次又一次地調(diào)用相同的函數(shù)(dataReceived
).同時在Erlang客戶端中,"真正"的運行進程(get_poetry/4
)形成通過"尾調(diào)優(yōu)化"一次又一次調(diào)用它們自己的循環(huán).
如果連接關(guān)閉了, get_poetry
做的最后一件事情是把詩歌發(fā)送到主進程.同時結(jié)束 get_poetry
正在運行的進程,因為沒有什么可做的了.
我們Erlang客戶端中剩下的關(guān)鍵函數(shù)是 collect_poems:
collect_poems(0, Poems) ->
[io:format("~s\n", [P]) || P <- Poems];
collect_poems(N, Poems) ->
receive
{'DOWN', _, _, _, _} ->
collect_poems(N-1, Poems);
{poem, Poem} ->
collect_poems(N, [Poem|Poems])
end.
這個函數(shù)被主進程運行,就像 get_poetry
,它對自身遞歸循環(huán).它同樣阻塞. receive
告訴進程等待符合給定模式的消息到來,并且從"信箱"中提取消息.
collect_poems
函數(shù)等待兩種消息: 詩歌和"DOWN"通知.后者是發(fā)送給主進程的, 當(dāng) get_poetry
進程之一由于某種原因死了的情況發(fā)送(這是 spawn_monitor
的監(jiān)控部分).通過數(shù) DOWN 消息,我們知道何時所有的詩歌都結(jié)束了. 前者是來自 get_poetry
進程的包含完整詩歌的消息.
OK,讓我們運行一下Erlang客戶端.首先啟動3個慢速服務(wù)器:
python blocking-server/slowpoetry.py --port 10001 poetry/fascination.txt
python blocking-server/slowpoetry.py --port 10002 poetry/science.txt
python blocking-server/slowpoetry.py --port 10003 poetry/ecstasy.txt --num-bytes 30
現(xiàn)在我們可以運行Erlang客戶端了,與Python客戶端有相似的命令行語法.如果你在Linux或其他UNIX-樣的系統(tǒng),你應(yīng)該可以直接運行客戶端(假設(shè)你安裝了Erlang并使得它在你的PATH上).在Windows中,你可能需要運行 escript
程序,將指向Erlang客戶端的路徑作為第一個參數(shù)(其他參數(shù)留給Erlang客戶端自身的參數(shù)).
./erlang-client-1/get-poetry 10001 10002 10003
之后,你可以看到如下輸出:
Task 3: got 30 bytes of poetry from 127:0:0:1:10003
Task 2: got 10 bytes of poetry from 127:0:0:1:10002
Task 1: got 10 bytes of poetry from 127:0:0:1:10001
...
這就像之前的Python客戶端之一,打印我們得到的每一小部分詩歌的信息.當(dāng)所有詩歌都結(jié)束后,客戶端應(yīng)該打印每首詩的完整內(nèi)容.注意客戶端在所有服務(wù)器之間切換,這取決于哪個服務(wù)器可以發(fā)送詩歌.
圖45展示了Erlang客戶端的進程結(jié)構(gòu):
http://wiki.jikexueyuan.com/project/twisted-intro/images/p20_erlang-3.png" alt="" /> 圖45 Erlang詩歌客戶端
這張圖顯示了3個 get_poetry
進程(每個服務(wù)器一個)和一個主進程.你可以看到消息從詩歌進程流向主進程.
那么當(dāng)一個服務(wù)器失敗了會發(fā)生什么呢? 讓我們試試:
./erlang-client-1/get-poetry 10001 10005
上面命令包含一個活動的端口(假設(shè)你沒有終止之前的詩歌服務(wù)器)和一個未激活的端口(假設(shè)你沒有在10005端口運行任一服務(wù)器). 我們得到如下輸出:
Task 1: got 10 bytes of poetry from 127:0:0:1:10001
=ERROR REPORT==== 25-Sep-2010::21:02:10 ===
Error in process <0.33.0> with exit value: {{badmatch,{error,econnrefused}},[{erl_eval,expr,3}]}
Task 1: got 10 bytes of poetry from 127:0:0:1:10001
Task 1: got 10 bytes of poetry from 127:0:0:1:10001
...
最終客戶端從活動的服務(wù)器完成詩歌下載,打印出詩歌并退出.那么 main
函數(shù)是怎樣得知那兩個進程完成工作了? 那個錯誤消息就是線索. 這個錯誤來自當(dāng) get_poetry
嘗試連接到服務(wù)器時沒有得到期望的值({ok, Socket}),而是得到一個連接被拒絕的錯誤.
Erlang進程中一個未處理的異常將使其"崩潰",這意味著進程停止運行并且它們所有資源被回收了.但主進程,它監(jiān)視所有 get_poetry
進程,當(dāng)任何進程無論因為何種原因停止運行時將收到一個DOWN消息.這樣,我們的客戶端就退出了而不是一直運行下去.
讓我們總結(jié)一下Twisted和Erlang客戶端關(guān)于并行化的特點:
在最后, 兩個客戶端中的 main
函數(shù)異步地接收詩歌和"任務(wù)完成"通知.在Twisted客戶端中這個信息是通過 Deferred
發(fā)送的,而在Erlang中客戶端接收來自內(nèi)部進程消息.
注意到兩個客戶端非常像,無論它們的整體策略還是代碼架構(gòu).但機理有一點點不同,一個是使用對象, deferreds
和回調(diào),另一個是使用進程和消息.然而在高層的思想模型方面,兩個客戶端是十分相似的,如果你熟悉兩種語言可以很方便地把一種轉(zhuǎn)化為另一種.
甚至 reactor
模式在Erlang客戶端中以小型化形式重現(xiàn).我們詩歌客戶端中的每個Erlang進程終究轉(zhuǎn)變?yōu)橐粋€遞歸循環(huán):
你可以把 Erlang 程序視作一系列小 reactor
的大集合,每個都自己旋轉(zhuǎn)著并且偶爾向另一個小 reactor
發(fā)送一個信息(它將以另一個事件來處理這個信息).
另外如果你更加深入Erlang,你將發(fā)現(xiàn)回調(diào)露面了. Erlang的 gen_server 進程是一個通用的 reactor
循環(huán),你可以用一系列回調(diào)函數(shù)來"實例化"它,這是一種在Erlang系統(tǒng)中重復(fù)出現(xiàn)的模式.
在這個部分我們關(guān)注Twisted與Erlang的相似性,但它們畢竟有很多不同.Erlang的一個獨特特性之一是它處理錯誤的方式.一個大的Erlang程序被結(jié)構(gòu)化為一個樹形結(jié)構(gòu)的進程組,在高一層有"監(jiān)管者",在葉子上有"工作者".如果一個工作進程崩潰了,監(jiān)管進程會注意到并采取相應(yīng)行動(通常重啟失敗的進程).
如果你有興趣學(xué)習(xí)Erlang,那么很幸運.許多關(guān)于Erlang的書已經(jīng)出版或?qū)⒁霭?
Armstrong
的書,并且在許多關(guān)鍵部分深入更多細(xì)節(jié).關(guān)于Erlang先就這么多.在 下一部分 我們會看一看Haskell,另一種函數(shù)式語言,與Python和Erlang的感覺都不同.但我們將努力去發(fā)現(xiàn)一些共同點.
本部分原作參見: dave @ http://krondo.com/blog/?p=2692
本部分翻譯內(nèi)容參見luocheng @ https://github.com/luocheng/twisted-intro-cn/blob/master/p20.rst