鍍金池/ 教程/ Python/ 輪子內(nèi)的輪子: Twisted和Erlang
小插曲 Deferred
異步編程模式與Reactor初探
使用Deferred新功能實現(xiàn)新客戶端
由twisted支持的客戶端
增強defer功能的客戶端
改進詩歌下載服務(wù)器
測試詩歌
更加"抽象"的運用Twisted
Deferred用于同步環(huán)境
輪子內(nèi)的輪子: Twisted和Erlang
Twisted 進程守護
構(gòu)造"回調(diào)"的另一種方法
Twisted 理論基礎(chǔ)
惰性不是遲緩: Twisted和Haskell
第二個小插曲,deferred
使用Deferred的詩歌下載客戶端
Deferreds 全貌
結(jié)束
取消之前的意圖
由Twisted扶持的客戶端
改進詩歌下載服務(wù)器
初識Twisted

輪子內(nèi)的輪子: Twisted和Erlang

簡介

在這個系列中,有一個事實我們還沒有介紹,即混合同步的"普通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ù).

回顧回調(diào)

考慮 圖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

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詩歌代理

讓我們看一下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/3get_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)于并行化的特點:

  1. 它們都是同時連接到所有詩歌服務(wù)器(或嘗試連接).
  2. 它們都是從服務(wù)器立刻接收詩歌,無論是哪個服務(wù)器發(fā)送的.
  3. 它們都是以小段方式處理詩歌,因此必須保存迄今為止收到的詩歌的一部分.
  4. 它們都創(chuàng)建一個"對象"(或者Python對象或者Erlang進程)來為一個特定服務(wù)器處理所有工作.
  5. 它們都需要小心地確定詩歌何時結(jié)束,無論一個特定的下載成功與否.

在最后, 兩個客戶端中的 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):

  1. 等待一些事情發(fā)生(一小部分詩歌到來,一首詩傳遞完畢,另一個進程結(jié)束),以及
  2. 采取相應(yīng)的行動.

你可以把 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ū)⒁霭?

  • Programming Erlang —— 作者是Erlang的發(fā)明者之一.這個語言的精彩入門.
  • Erlang Programming —— 此書補充了 Armstrong 的書,并且在許多關(guān)鍵部分深入更多細(xì)節(jié).
  • Erlang and OTP in Action —— 此書尚未出版,但我正在等待.前兩本書都沒有介紹OTP,構(gòu)造大型應(yīng)用的Erlang框架.完全披露:這本書的兩個作者是我的朋友.

關(guān)于Erlang先就這么多.在 下一部分 我們會看一看Haskell,另一種函數(shù)式語言,與Python和Erlang的感覺都不同.但我們將努力去發(fā)現(xiàn)一些共同點.

建議練習(xí)(為高度熱情的人)

  1. 瀏覽Erlang和Python客戶端,并且確定它們在哪里相同哪里不同.它們是怎樣處理錯誤的(比如連接到詩歌服務(wù)器的一個錯誤)?
  2. 簡化Erlang客戶端以便它不再打印到來的詩歌片段(故而你也不需要跟蹤任務(wù)號).
  3. 修改Erlang客戶端來計量下載每個詩歌所用的時間.
  4. 修改Erlang客戶端打印詩歌,并使詩歌的順序與它們在命令行給定的相同.
  5. 修改Erlang客戶端來打印一個更加可讀的錯誤信息當(dāng)我們不能連接到詩歌服務(wù)器時.
  6. 寫一個Erlang版本的詩歌服務(wù)器正如我們在Twisted中寫的.

參考

本部分原作參見: dave @ http://krondo.com/blog/?p=2692

本部分翻譯內(nèi)容參見luocheng @ https://github.com/luocheng/twisted-intro-cn/blob/master/p20.rst

上一篇:Deferreds 全貌下一篇:初識Twisted