(轉) Twisted :第二十部分 輪子中的輪子: Twisted和Erlang

簡介

在這個系列中,有一個事實咱們尚未介紹,即混合同步的"普通Python"代碼與異步Twisted代碼不是一個簡單的任務,由於在Twisted程序中阻滯不定時間將使異步模型的優點喪失殆盡.html

若是你是初次接觸異步編程,那麼你獲得的知識看起來有一些侷限.你能夠在Twisted框架內使用這些新技術,而不是在更廣闊的通常Python代碼世界中.同時,當用Twisted工做時,你僅僅侷限於那些專門爲做爲Twisted程序一部分所寫的庫,至少若是你想直接從reactor 線程調用它們.python

可是異步編程技術已經存在了不少年而且幾乎不侷限於Twisted.其實僅在Python中就有使人吃驚數目的異步編程模型. 搜索 一下就會看到不少. 它們在細節方面不一樣於Twisted,可是基本的思想(如異步I/O,將大規模數據流分割爲小塊處理)是同樣的.因此若是你須要,或者選擇,使用一個不一樣的框架,你將因爲學習了Twisted而具有一個很好的開端.react

當咱們移步Python以外,一樣會發現不少語言和系統要麼基於要麼使用了異步編程模型.你在Twisted學習到的知識將繼續爲你在異步編程方面開拓更廣闊的領域而服務.git

在這個部分,咱們將簡單地看一看 Erlang,一種編程語言和運行時系統,它普遍使用異步編程概念,可是以一種獨特的方式.請注意咱們不是要開始寫 Erlang入門.而是稍稍探索一下Erlang中包含的一些思想,看看這些與Twisted思想的聯繫.基本主題就是你經過學習Twisted獲得的知識能夠應用到學習其餘技術.程序員


回顧回調

考慮 圖6 ,回調的圖形表示. 是 :doc:`p06` 中介紹的 詩歌代理3.0 的回調和 dataReceived 方法中的順序詩歌客戶端的原理. 每次從一個相連的詩歌服務器下載一小部分詩歌時將激發回調.github

假設咱們的客戶端從3個不一樣的服務器下載3首詩.以 reactor 的角度看問題(這是在這個系列中一直主張的),咱們獲得一個單一的大循環,當每次輪到時激發一個或多個回調,如圖40:編程

_static/p20_reactor-2.png

圖40: 以 reactor 角度的回調服務器

此圖顯示了 reactor 歡快地運轉,每次詩歌到來時它調用 dataReceived. 每次 dataReceived 調用應用於一個特定的PoetryProtocal 類實例. 咱們知道一共有3個實例由於咱們正在下載3首詩(因此必須有3個鏈接).網絡

以一個Protocol實例的角度考慮這張圖.記住每一個Protocol只有一個鏈接(一首詩). 那個實例可「看到」一個方法調用流,每一個方法承載着詩歌的下一部分,以下:多線程

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")
...

然而這不是嚴格意義上的Python循環,咱們能夠將其概念化爲一個循環:

for data in poetry_stream(): # pseudo-code
    dataReceived(data)

咱們能夠設想"回調循環",如圖41:

_static/p20_callback-loop.png

圖41:一個虛擬回調循環

一樣,這不是一個 for 循環或 while 循環. 在咱們詩歌客戶端中惟一重要的Python循環是 reactor. 可是咱們能夠把每一個Protocol視做一個虛擬循環,當有詩歌到來時它會啓動循環. 根據這種想法, 咱們能夠用圖42重構整個客戶端:

_static/p20_reactor-3.png

圖42: reactor 轉動虛擬循環

在這張圖中,有一個大循環 —— reactor 和三個虛擬循環 —— 詩歌協議實例個體.大循環轉起來,如此,使得虛擬循環也轉起來了,就像一組環環相扣的齒輪.


進入Erlang

Erlang,與Python同樣,源自一種八十年代建立的通常目的動態類型的編程語言.不像Python的是,Erlang是功能型的而不是面向對象的,而且在句法上相似懷舊的 Prolog, Erlang最初就是由其實現的. Erlang被設計爲創建高度可靠的分佈式電話系統,這樣Erlang包含普遍的網絡支持.

Erlang的一個最獨特的特性是一個涉及輕量級進程的併發模型. 一個Erlang進程既不是一個操做系統進程也不是線程.而它是在Erlang運行環境中一個獨立運行的函數,它有本身的堆棧.Erlang進程不是輕量級的線程,由於Erlang進程不能共享狀態(許多數據類型也是不可變的,Erlang是一種功能性編程語言).一個Erlang進程能夠與其餘Erlang進程交互,但僅僅是經過發送消息,消息老是,至少概念上,被複制的而不是共享.

因此一個Erlang程序看起來如圖43:

_static/p20_erlang-11.png

圖43:有3個進程的Erlang程序

在此圖中,個體進程變成了"真實的".由於進程在Erlang中是第一構造,就像Python中的對象.但運行時變成了"虛擬的",不是因爲它不存在,而是因爲它不是一個簡單的循環.Erlang運行時多是多線程的,由於它必須去實現一個全面的編程語言,還要負責不少除異步I/O以外的東西.進一步,一個語言運行時也就是容許Erlang進程和代碼執行的媒介,而不是像Twisted中的 reactor 那樣的額外構造.

因此一個Erlang程序的更好表示以下圖44:

_static/p20_erlang-2.png

圖44: 有若干進程的Erlang程序

固然, Erlang運行時確實須要使用異步I/O以及一個或多個選擇循環,由於Erlang容許你建立 大量 進程. 大規模Erlang程序能夠啓動成千上萬的Erlang進程,因此爲每一個進程分配一個實際地OS線程是問題所在.若是Erlang容許多進程執行I/O,同時容許其餘進程運行即使那個I/O阻塞了,那麼異步I/O就必須被包含進來了.

注咱們關於Erlang程序的圖說明了每一個進程是"靠它本身的力量"運行,而不是被回調旋轉着. 隨着 reactor 的工做被概括成Erlang運行時的結構,回調再也不扮演中心角色. 原來在Twisted中須要經過回調解決的問題,在Erlang中將經過從一個進程向另外一個進程發送異步消息來解決.


一個Erlang詩歌代理

讓咱們看一下Erlang詩歌客戶端. 此次咱們直接跳入工做版本而不是像在Twisted中慢慢地搭建它.一樣,這不是意味着完整版本的Erlang介紹. 但若是這激起了你的興趣,咱們在本部分最後建議了一些深度閱讀資料.

Erlang客戶端位於 erlang-client-1/get-poetry. 爲了運行它,你固然須要安裝 Erlang.

下面代碼是 main 函數代碼,與Python客戶端中的 main 函數具備相同的目的:

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的句法將顯得有一點奇怪.可是有一些人也這樣認爲Python.

main 函數被兩個分離的句羣定義,被分號分割. Erlang根據參數選擇運行哪個句羣,因此第一個句羣只在咱們執行客戶端時不提供任何命令行參數的狀況下運行,而且它只打印出幫助信息.第二個句羣是全部實際的行動.

Erlang函數中的每條語句被逗號分隔,因此函數以句號結尾.讓咱們看一看第二個句羣,第一行僅僅分析命令行參數而且將它們綁定到一個變量(Erlang中全部變量必須大寫).第二行使用 self 函數來獲取當下正在運行的Erlang進程(而非OS進程)的ID.因爲這是主函數,你能夠認爲它等價於Python中的 __main__ 模塊. 第三行是最有趣的:

[erlang:spawn_monitor(fun () -> get_poetry(TaskNum, Addr, Main) end)
     || {TaskNum, Addr} <- enumerate(Addresses)],

這個語句是對Erlang列表的理解,與Python有類似的句法.它產生新的Erlang進程,對應每一個須要鏈接的服務器. 同時每一個進程將運行相同的 get_poetry 函數, 可是根據特定的服務器用不一樣的參數.咱們同時傳遞主進程的PID以便新的進程能夠把詩歌發送回來(你一般須要一個進程的PID來向它發送消息)

main 函數中的最後一條語句調用 collect_poems 函數,它等待詩歌傳回來和 get_poetry 進程結束.咱們能夠看一下其餘函數,但首先你可能會對比一下Erlang的 main 函數與等價地Twisted客戶端中的 main 函數.

如今讓咱們看一下Erlang中的 get_poetry 函數.事實上在咱們的腳本中有兩個函數叫 get_poetry.在Erlang中,一個函數被名字和元數同時肯定,因此咱們的腳本包含兩個不一樣的函數, get_poetry/3 和 get_poetry/4,它們分別接收3個或4個參數.這裏是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, []).

這個函數首先作一個TCP鏈接,就像Twisted客戶端中的 get_poetry.但以後,不是返回,而是繼續使用那個TCP鏈接,經過調用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函數正在作Twisted客戶端中 PoetryProtocol 的工做,不一樣的是它使用阻塞函數調用. gen_tcp:recv 函數等待在套接字上一些數據的到來(或者套接字關閉),不管要等多長時間.但Erlang中的"阻塞"函數僅阻塞正在運行函數的進程,而不是整個Erlang運行時.那個TCP套接字並非一個真正的阻塞套接字(你不能在純Erlang代碼中建立一個真正的阻塞套接字).對於Erlang中的每一個套接字,在運行時的某處,一個"真正的"TCP套接字被設置爲非阻塞模型而且用做選擇循環的一部分.

可是Erlang進程並不知道這些.它僅僅等待一些數據的到來,若是阻塞了,其餘Erlang進程會代替運行.甚至一個進程從不阻塞,Erlang運行時能夠在任什麼時候刻自由地在進程間切換.換句話說,Erlang具備一個非協同併發機制.

注意 get_poetry/4,在收到一小部分詩歌后,繼續遞歸地調用它本身.對於一個急迫的語言程序員這看起來像耗盡內存的良方,但Erlang編譯器卻能夠優化"尾"調用(函數調用一個函數中的最後一條語句)爲循環.這照亮了又一個有趣的Erlang客戶端和Twisted客戶端之間的平行對比.在Twisted客戶端中,"虛擬"循環是被 reaactor 建立的,它一次又一次地調用相同的函數(dataReceived).同時在Erlang客戶端中,"真正"的運行進程(get_poetry/4)造成經過"尾調優化"一次又一次調用它們本身的循環.感受怎麼樣.

若是鏈接關閉了, get_poetry 作的最後一件事情是把詩歌發送到主進程.同時結束 get_poetry 正在運行的進程,由於剩下沒什麼可作的了.

咱們Erlang客戶端中剩下的關鍵函數是 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.

這個函數被主進程運行,就像 get_poetry,它對自身遞歸循環.它一樣阻塞. receive 告訴進程等待符合給定模式的消息到來,而且從"信箱"中提取消息.

collect_poems 函數等待兩種消息: 詩歌和"DOWN"通知.後者是發送給主進程的, 當 get_poetry 進程之一因爲某種緣由死了的狀況發送(這是 spawn_monitor 的監控部分).經過數 DOWN 消息,咱們知道什麼時候全部的詩歌都結束了. 前者是來自 get_poetry 進程的包含完整詩歌的消息.

OK,讓咱們運行一下Erlang客戶端.首先啓動3個慢服務器:

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

如今咱們能夠運行Erlang客戶端了,與Python客戶端有類似的命令行句法.若是你在Linux或其餘UNIX-樣的系統,你應該能夠直接運行客戶端(假設你安裝了Erlang並使得它在你的PATH上).在Windows中,你可能須要運行 escript 程序,將指向Erlang客戶端的路徑做爲第一個參數(其餘參數留給Erlang客戶端自身的參數).

./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客戶端之一,打印咱們獲得的每一小部分詩歌的信息.當全部詩歌都結束後,客戶端應該打印每首詩的完整內容.注意客戶端在全部服務器之間切換,這取決於哪一個服務器能夠發送詩歌.

圖45展現了Erlang客戶端的進程結構:

_static/p20_erlang-3.png

圖45: Erlang詩歌客戶端

這張圖顯示了3個 get_poetry 進程(每一個服務器一個)和一個主進程.你能夠看到消息從詩歌進程流向主進程.

那麼當一個服務器失敗了會發生什麼呢? 讓咱們試試:

./erlang-client-1/get-poetry 10001 10005

上面命令包含一個活動的端口(假設你沒有終止以前的詩歌服務器)和一個未激活的端口(假設你沒有在10005端口運行任一服務器). 咱們獲得以下輸出:

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
...

最終客戶端從活動的服務器完成詩歌下載,打印出詩歌並退出.那麼 main 函數是怎樣得知那兩個進程完成工做了? 那個錯誤消息就是線索. 這個錯誤源自當 get_poetry 嘗試鏈接到服務器時沒有獲得指望的值({ok, Socket}),而是獲得一個鏈接被拒絕的錯誤.

Erlang進程中一個未處理的異常將使其"崩潰",這意味着進程中止運行而且它們全部資源被回收了.但主進程,它監視全部 get_poetry 進程,當任何進程不管由於何種緣由中止運行時將收到一個DOWN消息.這樣,咱們的客戶端就退出了而不是一直運行下去.


討論

讓咱們總結一下Twisted和Erlang客戶端關於並行化的特色:

  1. 它們都是同時鏈接到全部詩歌服務器(或嘗試鏈接).

  2. 它們都是從服務器即刻接收詩歌,不管是哪一個服務器發送的.

  3. 它們都是以小段方式處理詩歌,所以必須保存迄今爲止收到的詩歌的一部分.

  4. 它們都建立一個"對象"(或者Python對象或者Erlang進程)來爲一個特定服務器處理全部工做.

  5. 它們都須要當心地肯定詩歌什麼時候結束,不管一個特定的下載成功與否.

在最後, 兩個客戶端中的 main 函數異步地接收詩歌和"任務完成"通知.在Twisted客戶端中這個信息是經過 Deferred 發送的,而在Erlang中客戶端接收來自內部進程消息.

注意到兩個客戶端很是像,不管它們的總體策略仍是代碼架構.但機理有一點點不一樣,一個是使用對象, deferreds 和回調,另外一個是使用進程和消息.然而在高層的思想模型方面,兩個客戶端是十分類似的,若是你熟悉兩種語言能夠很方便地把一種轉化爲另外一種.

甚至 reactor 模式在Erlang客戶端中以小型化形式重現.咱們詩歌客戶端中的每一個Erlang進程終究轉變爲一個遞歸循環:

  1. 等待一些事情發生(一小部分詩歌到來,一首詩傳遞完畢,另外一個進程結束),以及

  2. 採起相應的行動.

你能夠把 Erlang 程序視做一系列小 reactor 的大集合,每一個都本身旋轉着而且偶爾向另外一個小 reactor 發送一個信息(它將以另外一個事件來處理這個信息).

另外若是你更加深刻Erlang,你將發現回調露面了. Erlang的 gen_server 進程是一個通用的 reactor 循環,你能夠用一系列回調函數來"實例化"它,這是一種在Erlang系統中重複出現的模式.


進一步閱讀

在這個部分咱們關注Twisted與Erlang的類似性,但它們畢竟有不少不一樣.Erlang的一個獨特特性之一是它處理錯誤的方式.一個大的Erlang程序被結構化爲一個樹形結構的進程組,在高一層有"監管者",在葉子上有"工做者".若是一個工做進程崩潰了,監管進程會注意到並採起相應行動(一般重啓失敗的進程).


關於Erlang先就這麼多.在 下一部分 咱們會看一看Haskell,另外一種功能性語言,但與Python和Erlang的感受都不一樣.然而,咱們將努力去發現一些共同點.


建議練習(爲高度熱情的人)

  1. 瀏覽Erlang和Python客戶端,而且肯定它們在哪裏相同哪裏不一樣.它們是怎樣處理錯誤的(好比鏈接到詩歌服務器的一個錯誤)?

  2. 簡化Erlang客戶端以便它再也不打印到來的詩歌片斷(故而你也不須要跟蹤任務號).

  3. 修改Erlang客戶端來計量下載每一個詩歌所用的時間.

  4. 修改Erlang客戶端打印詩歌,並使詩歌的順序與它們在命令行給定的相同.

  5. 修改Erlang客戶端來打印一個更加可讀的錯誤信息當咱們不能鏈接到詩歌服務器時.

  6. 寫一個Erlang版本的詩歌服務器正如咱們在Twisted中寫的.

相關文章
相關標籤/搜索