并发编程
实现在单个程序中同时执行多个过程的能力(与同时运行多个程序相反)一直要求对人们使用传统功能编程语言进行编程的方式进行更改。 并发(或多线程)问题是确保要更新信息的线程仅影响正在处理的数据和信息。 例如,您不希望多次执行更新单个文件的过程,因为您可能会在该过程中损坏文件。
Erlang采用的方法是所有程序都是并发的,并且组件(功能和模块)从不共享数据。 而是使用消息在组件之间交换数据。 使用消息有助于通过限制单个组件修改数据的方式来消除并发数据修改问题。 而不是直接更改数据,而是发送一条消息来更新数据,这使得隔离和并发更新更加困难。 Erlang还基于操作将失败的原则进行工作,因此,Erlang具有一个能够处理错误并在必要时从错误中恢复的系统。
在内部,Erlang通过创建称为进程的小型轻量级执行来处理并发问题。 与其他主要语言(例如C)不同,这些进程不是建立在本机操作系统进程甚至线程模型之上,而是由Erlang虚拟机在内部创建和管理。 与本地OS线程相比,这使每个进程在内存和CPU要求上的重量明显减少。 此外,由于Erlang是通过自动创建和使用许多小进程来进行操作的,因此即使使用相对简单的程序,通常也要运行成千上万个进程。
并发模型内置于Erlang中,因此,如此大量的进程的管理是Erlang工作方式的关键部分。 还内置了用于在进程之间发送数据的消息传递系统,该消息传递系统旨在将消息高效地分发到任何进程,而与当前正在运行的进程数无关。 作为消息实现的附带好处,消息不仅可以在内部发送,还可以通过网络发送,从而使Erlang通过在多个实例之间共享消息来支持跨机器的分布式编程。
Craft.io流程
由于Erlang流程系统非常轻巧,因此流程的创建非常简单明了,因为没有开销,因此没有理由担心其中的含义。 这意味着您可以出于任何原因创建新流程来帮助您的应用程序。
在实践中,您可以通过调用内置函数spawn()
创建进程,该函数具有以下调用结构: PID = spawn(module, function, arguments)
其中module
是模块名称, function
是函数名称, arguments
是要提供给函数的参数列表。 返回值(在上面的示例中为PID)是进程标识符。
例如,在本系列的前一部分中,我们创建的Fibonacci函数可以使用以下格式作为新进程调用: PID = spawn(fib,printfibo,[10])
。
请注意, spawn()
的最后一个参数是仅包含一个元素的列表,而不是直接调用该函数时将使用的单个参数。 结果是一个新进程,该进程将执行该函数,就像调用该函数一样: fib:printfibo(10)
。
spawn()
创建的新进程将继续执行,直到正常(即没有错误)或异常(发生某些故障)终止为止。
即使您调用的函数不存在,实际的生成过程本身也不会失败。 这减少了测试代码中的生成过程的需求。 错误记录器将处理和记录超出Erlang Shell中的过程中的错误,这是一个处理错误报告的内置过程。
在典型用法中,进程用于支持并发。 在上面给出的斐波那契示例中,在另一个进程中运行printfibo()
函数不会产生有用的返回值。 但是,如果您想产生一个新的进程,例如底层的fibo()
函数,并同时向该进程发送信息和从该进程接收信息,该怎么办?
内置消息传递系统处理此交互。
讯息传递
Erlang中的消息传递系统是Erlang执行环境的另一个内置部分,可与流程系统结合使用,以实现数据和消息的高效交换。
每个进程都有一个“邮箱”,另一个进程可以在其中发送消息。 邮件按发送顺序存储,这意味着,如果您从一个进程向另一个进程发送已发送的两条消息(A,然后是B),则该消息出现在邮箱中,其中消息A优先,消息B第二个。 由于流程系统具有并发性,因此从多个流程到单个流程的消息的排序不会以任何特定的方式进行排序,除了来自同一流程的消息的各个顺序之外。
要将消息发送到流程,您需要知道要与之通信的流程的流程ID。 然后,您使用以下构造: Pid ! Message
Pid ! Message
,其中Pid
是进程ID, Message
是任何Erlang数据类型。
要从流程内部接收消息,请使用receive
语句。 在receive
语句中,您可以使用模式匹配来根据消息内容确定要执行的操作。 如果匹配成功,则从邮箱接收消息,通过将它们绑定到匹配中的变量来使消息参数可用,并执行相应的子句。
例如,与提供“ store”原子和一个值的消息进行匹配可能类似于清单1 。
清单1.使用receive
语句从流程内部接收消息
receive
{store, Value} -> store(Value),
{get, Value} -> get(Value)
end
在此示例中,代码使用了模式匹配来匹配左侧的原子和变量,然后在右侧执行操作,在这种情况下,将存储值并根据消息的内容获取值。
receive
语句会暂停该过程的执行,从而确保该过程现在正在等待,直到出现新消息,然后才能执行操作。 一个典型的示例是存储可能在多个进程之间共享的变量的基本操作。
通常,在应用程序中,您将使用receive
语句作为循环的一部分,以逐步读取发送到流程的新消息并执行单独的操作。 正如在第1部分中给出的Fibonacci示例中所见,Erlang没有传统意义上的循环(请参阅参考资料 )。 相反,您编写了一个调用自身以处理下一条消息的函数(请参见清单2 )。
清单2.调用自身以处理下一条消息的函数
dbrequest() ->
receive
{store, Value} -> store(Value),
{get, Value} -> get(Value)
end
在传统的并发环境中,您可能使用诸如信号灯之类的解决方案,该解决方案使进程可以确定变量是“正在使用”还是可以更新。 在许多环境中,信号量会等待每个进程尝试更新相同的值,这可能会延迟程序的执行。 在Erlang中,您可以将流程中的各个操作包围起来,然后使用消息传递来处理更新。 因为消息是按顺序接收的,所以可以通过依次处理每个消息来分别执行每个操作, 如图1所示。
图1.使用消息传递处理单个值的更新
![该图显示了数据流:存储值,然后读取值,然后更新值,然后更新值,该值流入接收器,该接收器通过创建,检索,更新或删除与数据进行交互。](https://i-blog.csdnimg.cn/blog_migrate/26966757326154eb291cd3dd12926ef3.png)
这种基本的并发和消息传递结构是许多不同应用程序的基础。 例如,Facebook使用消息环境进行Facebook消息传递。 CouchDB是基于文档的数据库,使用MochiWeb作为基础在Web上提供其接口,它利用流程和消息传递系统来确保正确处理数据库更新和响应,而不会遭受其他数据库通常遭受的正常并发更新问题。
消息传递(尤其是与并发处理结合使用时)使程序能够顺序处理信息,即使可能有来自不同位置的多个请求也是如此。 这是其他语言的典型并发编程的主要问题之一,因为它可以共享数据和操作,而不必担心会破坏或破坏过程中的信息。 这消除了大多数并发编程问题遇到的主要痛点之一。
但是,并发只有在解决扩展解决方案和提高性能的问题时才能发挥到最大作用,尤其是在现代网络和Web应用程序中。 最终,您达到了需要多个服务器的地步。 幸运的是,Erlang也为该分布式编程问题提供了解决方案。
分布式编程
Erlang中的分布式编程建立在简单的网络服务器和消息传递系统的组合之上,我们已经在本文前面看到了消息传递系统,以提供发送和接收消息的机制,更重要的是,它支持所支持的远程过程调用通过诸如本地RPC和Web服务之类的环境。
值得注意的是,分布式不一定意味着不同的机器,它可能是两个不同的Erlang应用程序,它们希望彼此通信并共享信息或操作。 除了在识别您正在通信的系统外,Erlang在一般使用期间对系统的本地或远程性质没有任何区别。
要开始使用分布式编程,首先需要启动Erlang并为Erlang的每个实例赋予唯一的名称。 这将在标识期间使用,以便您可以将消息发送到Erlang的命名实例。 要在从命令行使用Erlang时设置名称,可以使用sname
命令行选项(请参见清单3 )。
清单3.使用sname命令行选项
$ erl -sname one
Erlang R13B04 (erts-5.7.5) [source] [64-bit] [rq:1] [async-threads:0]
Eshell V5.7.5 (abort with ^G)
(one@mammoth)1>
请注意提示是如何更改的,以提供名称和主机名,这是节点的唯一标识符,可以在Erlang中用于标识节点和在节点之间进行通信。
如果现在启动另一个Erlang shell实例,则可以设置一个不同的名称,如清单4所示。
清单4.设置其他名称
$ erl -sname two
Erlang R14B (erts-5.8.1) [source] [smp:8:8] [rq:8] [async-threads:0] [hipe]
[kernel-poll:false]
Eshell V5.8.1 (abort with ^G)
(two@mammoth)1>
在两个节点都运行的情况下,您可以使用net_adm:ping()
测试一个节点是否可以与另一节点通信(请参见清单5 )。
清单5.使用net_adm:ping()
测试一个节点是否可以与另一节点通信
(one@mammoth)3> net_adm:ping('two@mammoth').
pong
这表明实例一可以与实例二进行通信。
要在两个进程之间发送消息,请使用消息传递运算符的修改形式,其中包括接收者的进程ID以及节点名。 例如,如果一个进程已使用基本名称注册,则您将使用清单5中的代码发送一条消息。 您可以在您创建的第一个Erlang实例的外壳程序中键入以下内容: { basic, 'two@mammoth'} ! { self(), "message" }
{ basic, 'two@mammoth'} ! { self(), "message" }
。
在第二台主机上,如果将当前进程(即外壳程序)注册为“基本”,然后检索消息,则可以输出消息数据。 在实例一中发送消息之前,您将需要注册当前进程(请参见清单6 )。
清单6.注册当前过程
(two@mammoth)1> register(basic,self()).
true
现在,创建receive
语句以输出消息(请参见清单7 )。
清单7.创建receive
语句以输出消息
(two@mammoth)2> receive
(two@mammoth)2> {From,Message} -> io:format(Message ++ "~n")
(two@mammoth)2> end.
something
ok
您已成功在两个Erlang实例之间发送了一条消息。 他们本来可以轻松地来到世界的各个角落,而不是同一台机器。 一旦可以在两台计算机之间发送消息,发送和接收任何类型的数据就成为了解进程ID和节点名称的简单情况。
对于更熟悉的远程过程调用,Erlang还支持rpc:call()
函数: rpc:call(Node, Module, Function, Arguments)
。
这将调用远程节点,并使用提供的参数执行特定的模块和功能,并将结果返回给调用方。 rpc模块包括允许同步,异步和阻塞调用的扩展。 这是一个直接函数调用,因此,如果许多客户端同时运行它,可能会引起并发问题,因此与消息传递模型相比可能并不理想,但这取决于您的应用程序。
使用MochiWeb
MochiWeb是在Erlang之上构建的整个HTTP Web堆栈。 它利用了本文中介绍的Erlang的许多功能,包括使用消息传递和过程来提供高性能和高并发性。 MochiWeb利用流程系统来支持并发,并使用消息来帮助处理请求并累积结果。
MochiWeb本身是代码和一组脚本的组合,使您能够快速创建一个基本框架,从中可以构建和扩展自己的应用程序。 在最后一部分中,我们将研究如何使用MochiWeb,设置新的Web服务器应用程序以及如何扩展它以支持自己的应用程序。
获取MochiWeb的最简单方法是从GitHub获取源代码。 您可以通过使用git
命令或通过从GitHub网站下载可下载的软件包来执行此操作(请参阅参考资料) 。
要使用git获取源代码,请使用: $ git clone https://github.com/mochi/mochiweb.git
。
这将在当前目录中创建一个名为mochiweb的目录。 要使用MochiWeb,您需要构建源。 这将准备MochiWeb,以便您可以创建一个新的MochiWeb应用程序。
为此,首先在mochiweb目录中运行make,如清单8所示。
清单8.构建资源
$ cd mochiweb $ make
==> mochiweb (get-deps)
==> mochiweb (compile)
Compiled src/mochiweb_sup.erl
Compiled src/mochifmt.erl
Compiled src/mochiweb_charref.erl
Compiled src/mochiweb_request_tests.erl
Compiled src/mochifmt_records.erl
Compiled src/mochiweb_socket.erl
Compiled src/mochiweb_app.erl
Compiled src/mochiweb_io.erl
Compiled src/mochifmt_std.erl
Compiled src/mochiglobal.erl
Compiled src/mochiweb_socket_server.erl
Compiled src/mochijson.erl
Compiled src/mochihex.erl
Compiled src/mochiweb_html.erl
Compiled src/mochiweb_multipart.erl
Compiled src/mochilogfile2.erl
Compiled src/mochiweb_cover.erl
Compiled src/mochiweb_util.erl
Compiled src/mochitemp.erl
Compiled src/reloader.erl
Compiled src/mochinum.erl
Compiled src/mochiweb_headers.erl
Compiled src/mochiweb_skel.erl
Compiled src/mochiutf8.erl
Compiled src/mochiweb_echo.erl
Compiled src/mochiweb_acceptor.erl
Compiled src/mochiweb_http.erl
Compiled src/mochijson2.erl
Compiled src/mochiweb_cookies.erl
Compiled src/mochiweb.erl
Compiled src/mochiweb_mime.erl
Compiled src/mochilists.erl
Compiled src/mochiweb_response.erl
Compiled src/mochiweb_request.erl
现在已编译mochiweb源。 要创建可用于构建自己的Web服务器的示例应用程序框架,请再次使用make来构建新的项目目录。 PROJECT成为项目和目录名称,而PREFIX成为将在其中创建新PROJECT目录的目录名称。 例如,创建一个名为mywebserver的项目: $ make app PROJECT=mywebserver PREFIX=../
。
上一行将在父目录(即与mochiweb处于同一级别)内创建一个新的MochiWeb应用程序。
新创建的目录是基本的Web服务器,默认情况下将在端口8080(所有接口)上侦听。 要构建该应用程序,请再次运行make
以确保MochiWeb组件和单个应用程序已编译:
$ cd ../mywebserver
$ make
最后,运行start-dev.sh脚本以运行基本应用程序。 这将产生大量输出,所有输出均为“进度报告”,指示正在创建以处理Web服务器的各个进程。 如果输出中未报告任何错误,则您的Web服务器已启动并正在运行(请参见清单9 )。
清单9. Web服务器已启动并正在运行
Erlang R13B04 (erts-5.7.5) [source] [64-bit] [rq:1] [async-threads:0]
=PROGRESS REPORT==== 7-Apr-2011::11:40:36 ===
supervisor: {local,sasl_safe_sup}
started: [{pid,<0.42.0>},
{name,alarm_handler},
{mfa,{alarm_handler,start_link,[]}},
{restart_type,permanent},
{shutdown,2000},
{child_type,worker}]
=PROGRESS REPORT==== 7-Apr-2011::11:40:36 ===
supervisor: {local,sasl_safe_sup}
started: [{pid,<0.43.0>},
{name,overload},
{mfa,{overload,start_link,[]}},
{restart_type,permanent},
{shutdown,2000},
{child_type,worker}]
=PROGRESS REPORT==== 7-Apr-2011::11:40:36 ===
supervisor: {local,sasl_sup}
started: [{pid,<0.41.0>},
{name,sasl_safe_sup},
{mfa,
{supervisor,start_link,
[{local,sasl_safe_sup},sasl,safe]}},
{restart_type,permanent},
{shutdown,infinity},
{child_type,supervisor}]
=PROGRESS REPORT==== 7-Apr-2011::11:40:36 ===
supervisor: {local,sasl_sup}
started: [{pid,<0.44.0>},
{name,release_handler},
{mfa,{release_handler,start_link,[]}},
{restart_type,permanent},
{shutdown,2000},
{child_type,worker}]
=PROGRESS REPORT==== 7-Apr-2011::11:40:36 ===
application: sasl
started_at: mywebserver_dev@localhost
Eshell V5.7.5 (abort with ^G)
(mywebserver_dev@localhost)1> ** Found 0 name clashes in code paths
=PROGRESS REPORT==== 7-Apr-2011::11:40:36 ===
supervisor: {local,crypto_sup}
started: [{pid,<0.54.0>},
{name,crypto_server},
{mfa,{crypto_server,start_link,[]}},
{restart_type,permanent},
{shutdown,2000},
{child_type,worker}]
=PROGRESS REPORT==== 7-Apr-2011::11:40:36 ===
application: crypto
started_at: mywebserver_dev@localhost
** Found 0 name clashes in code paths
=PROGRESS REPORT==== 7-Apr-2011::11:40:36 ===
supervisor: {local,mywebserver_sup}
started: [{pid,<0.59.0>},
{name,mywebserver_web},
{mfa,
{mywebserver_web,start,
[[{ip,{0,0,0,0}},
{port,8080},
{docroot,
"/root/mybase/mywebserver/priv/www"}]]}},
{restart_type,permanent},
{shutdown,5000},
{child_type,worker}]
=PROGRESS REPORT==== 7-Apr-2011::11:40:36 ===
application: mywebserver
started_at: mywebserver_dev@localhost
=PROGRESS REPORT==== 7-Apr-2011::11:40:36 ===
supervisor: {local,kernel_safe_sup}
started: [{pid,<0.77.0>},
{name,timer_server},
{mfa,{timer,start_link,[]}},
{restart_type,permanent},
{shutdown,1000},
{child_type,worker}]
您可以通过打开Web浏览器来尝试访问新的Web服务器。 如果在同一台计算机上,则将打开http:// localhost:8080 /。 如果一切正常,您应该获得一个标题为“ It Worked”且消息为“ webserver running”的页面。
如果要修改新服务器,则可以在应用程序目录中编辑src / mywebserver_web.erl文件。 它包含用于运行和支持Web服务的核心代码。
该过程的核心是loop()
函数,该函数在主MochiWeb系统每次接收到请求时都会调用。 该函数提供了两个参数,即请求结构(包括请求类型,路径和任何正文数据)和DocRoot。 后者是必需的,因为默认情况下,服务器将在指定的文档根目录中提供从文件系统请求的文件。
请求的过程分为两个阶段,首先使用case语句提取请求类型( GET
, POST
等)。 然后,第二个case语句用于标识请求的路径。
您可以在Erlang中使用模式匹配,以便一侧的路径触发特定的响应。 例如,可以修改代码,以便访问服务器上的路径/ hello返回短语“ Hello world”,如清单10所示。
清单10. Erlang中的模式匹配
loop(Req, DocRoot) ->
"/" ++ Path = Req:get(path),
try
case Req:get(method) of
Method when Method =:= 'GET'; Method =:= 'HEAD' ->
case Path of
"congrat" ->
Req:ok({"text/html", [],["<h1>Congratulation
</h1>"]});
"hello" ->
Req:ok({"text/plain",[],["Hello world"]});
_ ->
Req:serve_file(Path, DocRoot)
end;
'POST' ->
case Path of
_ ->
Req:not_found()
end;
_ ->
Req:respond({501, [], []})
end
catch
Type:What ->
Report = ["web request failed",
{path, Path},
{type, Type}, {what, What},
{trace, erlang:get_stacktrace()}],
error_logger:error_report(Report),
%% NOTE: mustache templates need \ because they are not awesome.
Req:respond({500, [{"Content-Type", "text/plain"}],
"request failed, sorry\n"})
end.
编辑完文件后,请确保运行make来重建应用程序,然后使用start-dev.sh脚本重新启动应用程序。
现在,如果您使用http:// localhost:8080 / hello访问Web服务器的URL,您应该会收到hello world消息。
尽管这是一个基本示例,但是您可以看到,通过查找POST或PUT请求,处理文档主体然后存储信息,可以轻松实现添加新功能(如支持基本REST服务)的功能。 使用衍生的过程和消息传递,您可以将来自服务器的请求排队,以存储,更新和检索信息。
结论
Erlang在电话交换机环境中拥有悠久的历史,这意味着该语言的核心功能是在与大多数其他语言完全不同的环境中开发的。 该语言内置了运行和创建多个处理多个并发操作(例如电话)的问题。
通过使用内置的消息传递系统,可以简化创建新流程并在它们之间进行通信的方式,该方式既不具有破坏性,又不涉及复杂的信号量系统,以便在各个进程之间共享信息。 由于消息传递是顺序的,因此使每个进程之间的通信易于处理。 此外,消息系统可跨Erlang实例和网络运行,从而使跨机器通信变得简单而直接。
MochiWeb将此功能的大部分结合在一起,以产生一个高性能且高度可扩展的Web服务器解决方案,该解决方案也可以轻松扩展和扩展以支持额外的功能,而工作却很少。
翻译自: https://www.ibm.com/developerworks/opensource/library/os-erlang2/index.html