在這個(gè)系列中,有一個(gè)事實(shí)我們還沒有介紹,即混合同步的"普通Python"代碼與異步Twisted代碼不是一個(gè)簡(jiǎn)單的工作,因?yàn)樵赥wisted程序中阻滯不定時(shí)間將使異步模型的優(yōu)勢(shì)喪失殆盡.
如果你是初次接觸異步編程,那么你得到的知識(shí)看起來有一些局限.你可以在Twisted框架內(nèi)使用這些新技術(shù),而不是在更廣闊的一般Python代碼世界中.同時(shí),當(dāng)用Twisted工作時(shí),你僅僅局限于那些專門為了作為Twisted程序一部分所寫的庫,至少如果你想直接從 reactor 線程調(diào)用它們的時(shí)候.
但是異步編程技術(shù)已經(jīng)存在了很多年并且?guī)缀醪痪窒抻赥wisted.其實(shí)僅在Python中就有令人吃驚數(shù)量的異步編程模型. 搜索 一下就會(huì)看到很多. 它們?cè)诩?xì)節(jié)方面不同于Twisted,但是基本的思想(如異步I/O,將大規(guī)模數(shù)據(jù)流分割為小塊處理)是一樣的.所以如果你需要,或者選擇,使用一個(gè)不同的框架,你將會(huì)因?yàn)閷W(xué)習(xí)了Twisted而具備一個(gè)很好的開端.
當(dāng)我們移步Python之外,同樣會(huì)發(fā)現(xiàn)很多語言和系統(tǒng)基于或者使用異步編程模型.你在Twisted學(xué)習(xí)到的知識(shí)將繼續(xù)為你在異步編程方面開拓更廣闊的領(lǐng)域而服務(wù).
在這個(gè)部分,我們將簡(jiǎn)單地看一看 Erlang,一種編程語言和運(yùn)行時(shí)系統(tǒng),它以一種獨(dú)特的方式廣泛使用異步編程概念.請(qǐng)注意我們不是要開始寫 Erlang入門.而是稍稍探索一下Erlang中包含的一些思想,看看這些與Twisted思想的聯(lián)系.基本主題就是你通過學(xué)習(xí)Twisted得到的知識(shí)可以應(yīng)用到學(xué)習(xí)其他技術(shù).
考慮 圖6 ,回調(diào)的圖形表示. 是 第六部分 中介紹的 詩歌代理3.0 的回調(diào)和 dataReceived 方法中的順序詩歌客戶端的原理. 每次從一個(gè)相連的詩歌服務(wù)器下載一小部分詩歌時(shí)將激發(fā)回調(diào).
假設(shè)我們的客戶端從3個(gè)不同的服務(wù)器下載3首詩.以 reactor 的角度看問題(這是在這個(gè)系列中一直主張的),我們得到一個(gè)單一的大循環(huán),當(dāng)每次輪詢時(shí)激發(fā)一個(gè)或多個(gè)回調(diào),如圖40:

此圖顯示了 reactor 歡快地運(yùn)轉(zhuǎn),每次詩歌到來時(shí)它調(diào)用 dataReceived. 每次 dataReceived 調(diào)用應(yīng)用于一個(gè)特定的 PoetryProtocal 類實(shí)例. 我們知道一共有3個(gè)實(shí)例因?yàn)槲覀冋谙螺d3首詩(所以必須有3個(gè)連接).
以一個(gè)Protocol實(shí)例的角度考慮這張圖.記住每個(gè)Protocol只有一個(gè)連接(一首詩). 這個(gè)實(shí)例可“看到”一個(gè)方法調(diào)用流,每個(gè)方法接收著詩歌的下一部分,如下:
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),我們可以將其概念化為一個(gè)循環(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 一個(gè)虛擬回調(diào)循環(huán)
當(dāng)然這不是一個(gè) for 循環(huán)或 while 循環(huán). 在我們?cè)姼杩蛻舳酥形ㄒ恢匾腜ython循環(huán)是 reactor. 但是我們可以把每個(gè)Protocol視作一個(gè)虛擬循環(huán),當(dāng)有詩歌到來時(shí)它會(huì)啟動(dòng)循環(huán). 根據(jù)這種想法, 我們可以用圖42重構(gòu)整個(gè)客戶端:
http://wiki.jikexueyuan.com/project/twisted-intro/images/p20_reactor-3.png" alt="" /> 圖42 reactor 轉(zhuǎn)動(dòng)虛擬循環(huán)
在這張圖中,有一個(gè)大循環(huán) —— reactor 和三個(gè)虛擬循環(huán) —— 詩歌協(xié)議實(shí)例個(gè)體.大循環(huán)轉(zhuǎn)起來,如此,使得虛擬循環(huán)也轉(zhuǎn)起來了,就像一組環(huán)環(huán)相扣的齒輪.
Erlang,與Python一樣,源自一種八十年代創(chuàng)建的一般目的動(dòng)態(tài)類型的編程語言.不像Python,Erlang是功能型的而不是面向?qū)ο蟮?并且在句法上類似懷舊的 Prolog, Erlang最初就是由其實(shí)現(xiàn)的. Erlang被設(shè)計(jì)為建立高度可靠的分布式電話系統(tǒng),因此Erlang包含廣泛的網(wǎng)絡(luò)支持.
Erlang的一個(gè)最獨(dú)特的特性是一個(gè)涉及輕量級(jí)進(jìn)程的并發(fā)模型. 一個(gè)Erlang進(jìn)程既不是一個(gè)操作系統(tǒng)進(jìn)程也不是線程.它是在Erlang運(yùn)行環(huán)境中一個(gè)獨(dú)立運(yùn)行的函數(shù),它有自己的堆棧.Erlang進(jìn)程不是輕量級(jí)的線程,因?yàn)镋rlang進(jìn)程不能共享狀態(tài)(許多數(shù)據(jù)類型也是不可變的,Erlang是一種函數(shù)式編程語言).一個(gè)Erlang進(jìn)程可以與其他Erlang進(jìn)程交互,但僅僅是通過發(fā)送消息,消息總是(至少概念上)被復(fù)制的而不是共享.
所以一個(gè)Erlang程序看起來如圖43:
http://wiki.jikexueyuan.com/project/twisted-intro/images/p20_erlang-11.png" alt="" /> 圖43 有3個(gè)進(jìn)程的Erlang程序
在此圖中,個(gè)體進(jìn)程變成了"真實(shí)的存在".因?yàn)檫M(jìn)程在Erlang中是第一構(gòu)造,就像Python中的對(duì)象.但運(yùn)行時(shí)變成了"虛擬的",不是由于它不存在,而是由于它不是一個(gè)簡(jiǎn)單的循環(huán).Erlang運(yùn)行時(shí)可能是多線程的,因?yàn)樗仨毴?shí)現(xiàn)一個(gè)全面的編程語言,還要負(fù)責(zé)很多除異步I/O之外的東西.進(jìn)一步,一個(gè)語言運(yùn)行時(shí)也就是允許Erlang進(jìn)程和代碼執(zhí)行的媒介,而不是像Twisted中的 reactor 那樣的額外構(gòu)造.
所以一個(gè)Erlang程序的更好表示如下圖44:
http://wiki.jikexueyuan.com/project/twisted-intro/images/p20_erlang-2.png" alt="" /> 圖44 有若干進(jìn)程的Erlang程序
當(dāng)然, Erlang運(yùn)行時(shí)確實(shí)需要使用異步I/O以及一個(gè)或多個(gè)選擇循環(huán),因?yàn)镋rlang允許你創(chuàng)建 大量 進(jìn)程. 大規(guī)模Erlang程序可以啟動(dòng)成千上萬的Erlang進(jìn)程,所以為每個(gè)進(jìn)程分配一個(gè)實(shí)際地OS線程是問題所在.如果Erlang允許多進(jìn)程執(zhí)行I/O,同時(shí)允許其他進(jìn)程運(yùn)行即便那個(gè)I/O阻塞了,那么異步I/O就必須被包含進(jìn)來了.
注意我們關(guān)于Erlang程序的圖說明了每個(gè)進(jìn)程是"靠它自己的力量"運(yùn)行,而不是被回調(diào)旋轉(zhuǎn)著. 隨著 reactor 的工作被歸納成Erlang運(yùn)行時(shí)的結(jié)構(gòu),回調(diào)不再扮演中心角色. 原來在Twisted中需要通過回調(diào)解決的問題,在Erlang中將通過從一個(gè)進(jìn)程向另一個(gè)進(jìn)程發(fā)送異步消息來解決.
讓我們看一下Erlang詩歌客戶端. 這次我們直接跳入工作版本而不是像在Twisted中慢慢地搭建它.同樣,這意味著不是完整版本的Erlang介紹. 但如果激起了你的興趣,我們?cè)诒静糠肿詈笸扑]了一些深度閱讀資料.
Erlang客戶端位于 erlang-client-1/get-poetry. 為了運(yùn)行它,你需要安裝 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的句法將顯得有一點(diǎn)奇怪.但是有一些人也這樣認(rèn)為Python.main 函數(shù)被兩個(gè)分離的句群定義,被分號(hào)分割. Erlang根據(jù)參數(shù)選擇運(yùn)行哪一個(gè)句群,所以第一個(gè)句群只在我們執(zhí)行客戶端不提供任何命令行參數(shù)的情況下運(yùn)行,它只打印出幫助信息.第二個(gè)句群是所有實(shí)際的處理.
Erlang函數(shù)中的每條語句以逗號(hào)分隔,函數(shù)以句號(hào)結(jié)尾.讓我們看一看第二個(gè)句群,第一行僅僅分析命令行參數(shù)并且將它們綁定到一個(gè)變量(Erlang中所有變量必須大寫).第二行使用 self 函數(shù)來獲取當(dāng)下正在運(yùn)行的Erlang進(jìn)程(而非OS進(jìn)程)的ID.由于這是主函數(shù),你可以認(rèn)為它等價(jià)于Python中的 __main__ 模塊. 第三行是最有趣的:
[erlang:spawn_monitor(fun () -> get_poetry(TaskNum, Addr, Main) end)
|| {TaskNum, Addr} <- enumerate(Addresses)],
這個(gè)語句是對(duì)Erlang列表的理解,與Python有相似的句法.它對(duì)每個(gè)需要連接的服務(wù)器產(chǎn)生新的Erlang進(jìn)程. 同時(shí)每個(gè)進(jìn)程將運(yùn)行相同的 get_poetry 函數(shù), 但是根據(jù)特定的服務(wù)器用不同的參數(shù).我們同時(shí)傳遞主進(jìn)程的PID以便新的進(jìn)程可以把詩歌發(fā)送回來(你通常需要一個(gè)進(jìn)程的PID來向它發(fā)送消息)
main 函數(shù)中的最后一條語句調(diào)用 collect_poems 函數(shù),它等待詩歌傳回來和 get_poetry 進(jìn)程結(jié)束.我們可以看一下其他函數(shù),但首先你可能會(huì)對(duì)比一下Erlang的 main 函數(shù)與等價(jià)地Twisted客戶端中的 main 函數(shù).
現(xiàn)在讓我們看一下Erlang中的 get_poetry 函數(shù).事實(shí)上在我們的腳本中有兩個(gè)函數(shù)叫 get_poetry.在Erlang中,一個(gè)函數(shù)被名字和元數(shù)同時(shí)確定,所以我們的腳本包含兩個(gè)不同的函數(shù), get_poetry/3 和 get_poetry/4,它們分別接收3個(gè)或4個(gè)參數(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, []).
這個(gè)函數(shù)首先創(chuàng)建一個(gè)TCP連接,就像Twisted客戶端中的 get_poetry.但之后,不是返回,而是繼續(xù)使用那個(gè)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.
這個(gè)Erlang函數(shù)正在做Twisted客戶端中 PoetryProtocol 的工作,不同的是它使用阻塞函數(shù)調(diào)用. gen_tcp:recv 函數(shù)等待在套接字上一些數(shù)據(jù)的到來(或者套接字關(guān)閉),無論要等多長(zhǎng)時(shí)間.但Erlang中的"阻塞"函數(shù)僅阻塞正在運(yùn)行函數(shù)的進(jìn)程,而不是整個(gè)Erlang運(yùn)行時(shí).那個(gè)TCP套接字并不是一個(gè)真正的阻塞套接字(你不能在純Erlang代碼中創(chuàng)建一個(gè)真正的阻塞套接字).對(duì)于Erlang中的每個(gè)套接字,在運(yùn)行時(shí)的某處,一個(gè)"真正的"TCP套接字被設(shè)置為非阻塞模型并且用作選擇循環(huán)的一部分.
但是Erlang進(jìn)程并不知道這些.它僅僅等待一些數(shù)據(jù)的到來,如果阻塞了,其他Erlang進(jìn)程會(huì)代替運(yùn)行.甚至一個(gè)進(jìn)程從不阻塞,Erlang運(yùn)行時(shí)可以在任何時(shí)刻自由地在進(jìn)程間切換.換句話說,Erlang具有一個(gè)非協(xié)同并發(fā)機(jī)制.
注意 get_poetry/4,在收到一小部分詩歌后,繼續(xù)遞歸地調(diào)用它自己.對(duì)于一個(gè)急迫的語言程序員這看起來像耗盡內(nèi)存的良方,但Erlang編譯器卻可以優(yōu)化"尾"調(diào)用(函數(shù)調(diào)用一個(gè)函數(shù)中的最后一條語句)為循環(huán).這照亮了又一個(gè)有趣的Erlang客戶端和Twisted客戶端之間的平行對(duì)比.在Twisted客戶端中,"虛擬"循環(huán)是被 reaactor 創(chuàng)建的,它一次又一次地調(diào)用相同的函數(shù)(dataReceived).同時(shí)在Erlang客戶端中,"真正"的運(yùn)行進(jìn)程(get_poetry/4)形成通過"尾調(diào)優(yōu)化"一次又一次調(diào)用它們自己的循環(huán).
如果連接關(guān)閉了, get_poetry 做的最后一件事情是把詩歌發(fā)送到主進(jìn)程.同時(shí)結(jié)束 get_poetry 正在運(yùn)行的進(jìn)程,因?yàn)闆]有什么可做的了.
我們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.
這個(gè)函數(shù)被主進(jìn)程運(yùn)行,就像 get_poetry,它對(duì)自身遞歸循環(huán).它同樣阻塞. receive 告訴進(jìn)程等待符合給定模式的消息到來,并且從"信箱"中提取消息.
collect_poems 函數(shù)等待兩種消息: 詩歌和"DOWN"通知.后者是發(fā)送給主進(jìn)程的, 當(dāng) get_poetry 進(jìn)程之一由于某種原因死了的情況發(fā)送(這是 spawn_monitor 的監(jiān)控部分).通過數(shù) DOWN 消息,我們知道何時(shí)所有的詩歌都結(jié)束了. 前者是來自 get_poetry 進(jìn)程的包含完整詩歌的消息.
OK,讓我們運(yùn)行一下Erlang客戶端.首先啟動(dòng)3個(gè)慢速服務(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)在我們可以運(yùn)行Erlang客戶端了,與Python客戶端有相似的命令行語法.如果你在Linux或其他UNIX-樣的系統(tǒng),你應(yīng)該可以直接運(yùn)行客戶端(假設(shè)你安裝了Erlang并使得它在你的PATH上).在Windows中,你可能需要運(yùn)行 escript 程序,將指向Erlang客戶端的路徑作為第一個(gè)參數(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ù)器之間切換,這取決于哪個(gè)服務(wù)器可以發(fā)送詩歌.
圖45展示了Erlang客戶端的進(jìn)程結(jié)構(gòu):
http://wiki.jikexueyuan.com/project/twisted-intro/images/p20_erlang-3.png" alt="" /> 圖45 Erlang詩歌客戶端
這張圖顯示了3個(gè) get_poetry 進(jìn)程(每個(gè)服務(wù)器一個(gè))和一個(gè)主進(jìn)程.你可以看到消息從詩歌進(jìn)程流向主進(jìn)程.
那么當(dāng)一個(gè)服務(wù)器失敗了會(huì)發(fā)生什么呢? 讓我們?cè)囋?
./erlang-client-1/get-poetry 10001 10005
上面命令包含一個(gè)活動(dòng)的端口(假設(shè)你沒有終止之前的詩歌服務(wù)器)和一個(gè)未激活的端口(假設(shè)你沒有在10005端口運(yùn)行任一服務(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
...
最終客戶端從活動(dòng)的服務(wù)器完成詩歌下載,打印出詩歌并退出.那么 main 函數(shù)是怎樣得知那兩個(gè)進(jìn)程完成工作了? 那個(gè)錯(cuò)誤消息就是線索. 這個(gè)錯(cuò)誤來自當(dāng) get_poetry 嘗試連接到服務(wù)器時(shí)沒有得到期望的值({ok, Socket}),而是得到一個(gè)連接被拒絕的錯(cuò)誤.
Erlang進(jìn)程中一個(gè)未處理的異常將使其"崩潰",這意味著進(jìn)程停止運(yùn)行并且它們所有資源被回收了.但主進(jìn)程,它監(jiān)視所有 get_poetry 進(jìn)程,當(dāng)任何進(jìn)程無論因?yàn)楹畏N原因停止運(yùn)行時(shí)將收到一個(gè)DOWN消息.這樣,我們的客戶端就退出了而不是一直運(yùn)行下去.
讓我們總結(jié)一下Twisted和Erlang客戶端關(guān)于并行化的特點(diǎn):
在最后, 兩個(gè)客戶端中的 main 函數(shù)異步地接收詩歌和"任務(wù)完成"通知.在Twisted客戶端中這個(gè)信息是通過 Deferred 發(fā)送的,而在Erlang中客戶端接收來自內(nèi)部進(jìn)程消息.
注意到兩個(gè)客戶端非常像,無論它們的整體策略還是代碼架構(gòu).但機(jī)理有一點(diǎn)點(diǎn)不同,一個(gè)是使用對(duì)象, deferreds 和回調(diào),另一個(gè)是使用進(jìn)程和消息.然而在高層的思想模型方面,兩個(gè)客戶端是十分相似的,如果你熟悉兩種語言可以很方便地把一種轉(zhuǎn)化為另一種.
甚至 reactor 模式在Erlang客戶端中以小型化形式重現(xiàn).我們?cè)姼杩蛻舳酥械拿總€(gè)Erlang進(jìn)程終究轉(zhuǎn)變?yōu)橐粋€(gè)遞歸循環(huán):
你可以把 Erlang 程序視作一系列小 reactor 的大集合,每個(gè)都自己旋轉(zhuǎn)著并且偶爾向另一個(gè)小 reactor 發(fā)送一個(gè)信息(它將以另一個(gè)事件來處理這個(gè)信息).
另外如果你更加深入Erlang,你將發(fā)現(xiàn)回調(diào)露面了. Erlang的 gen_server 進(jìn)程是一個(gè)通用的 reactor 循環(huán),你可以用一系列回調(diào)函數(shù)來"實(shí)例化"它,這是一種在Erlang系統(tǒng)中重復(fù)出現(xiàn)的模式.
在這個(gè)部分我們關(guān)注Twisted與Erlang的相似性,但它們畢竟有很多不同.Erlang的一個(gè)獨(dú)特特性之一是它處理錯(cuò)誤的方式.一個(gè)大的Erlang程序被結(jié)構(gòu)化為一個(gè)樹形結(jié)構(gòu)的進(jìn)程組,在高一層有"監(jiān)管者",在葉子上有"工作者".如果一個(gè)工作進(jìn)程崩潰了,監(jiān)管進(jìn)程會(huì)注意到并采取相應(yīng)行動(dòng)(通常重啟失敗的進(jìn)程).
如果你有興趣學(xué)習(xí)Erlang,那么很幸運(yùn).許多關(guān)于Erlang的書已經(jīng)出版或?qū)⒁霭?
Armstrong 的書,并且在許多關(guān)鍵部分深入更多細(xì)節(jié).關(guān)于Erlang先就這么多.在 下一部分 我們會(huì)看一看Haskell,另一種函數(shù)式語言,與Python和Erlang的感覺都不同.但我們將努力去發(fā)現(xiàn)一些共同點(diǎn).
本部分原作參見: dave @ http://krondo.com/blog/?p=2692
本部分翻譯內(nèi)容參見luocheng @ https://github.com/luocheng/twisted-intro-cn/blob/master/p20.rst