(转) 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中写的.

相关文章
相关标签/搜索