函数式语言(特别是Erlang)如何/为什么能很好地扩展?
我一直在观察函数式编程语言和function的日益增长的可见性。 我看着他们,没有看到上诉的原因。
然后,最近我在Codemash参加了Kevin Smith的“Erlang基础”演示。
我喜欢这个演示文档,并了解到函数式编程的很多属性使得避免线程/并发问题变得更容易。 我知道状态和可变性的缺乏使得multithreading无法改变相同的数据,但凯文说(如果我理解正确的话)所有的通信都是通过消息发生的,消息是同步处理的(同样避免了并发问题)。
但是我已经读过Erlang用于高度可扩展的应用程序(爱立信首先创build它的全部原因)。 如果每件事都作为一个同步处理的消息来处理,那么如何有效地处理每秒数千个请求呢? 这不就是为什么我们开始走向asynchronous处理 – 所以我们可以利用同时运行多个线程的操作,并实现可扩展性? 看起来这样的架构虽然更安全,但在可扩展性方面却倒退了一步。 我错过了什么?
我了解Erlang的创build者故意避免支持线程以避免并发问题,但我认为multithreading对于实现可伸缩性是必需的。
函数式编程语言如何能够固有地线程安全,但仍然可以扩展?
函数式语言(通常)不依赖于variables的变异。 因此,我们不必保护variables的“共享状态”,因为这个值是固定的。 这反过来又避免了传统语言在整个处理器或机器上实现algorithm所必须经历的大部分环箍跳跃。
Erlang通过在一个消息传递系统中进行烘焙,使其能够在传统的function语言的基础上进一步发展,在一个基于事件的系统中,每个代码只需要担心接收消息和发送消息,而不用担心更大的图像。
这意味着程序员(名义上)不关心消息是在另一个处理器或机器上处理的:简单地发送这个消息足以让它继续下去。 如果它关心回应,它会等待它作为另一个消息 。
这样做的最终结果是每个片段都独立于其他片段。 没有共享的代码,没有共享的状态和来自消息系统的可以分布在许多硬件(或不是)中的所有交互。
将其与传统系统进行对比:我们必须将互斥体和信号置于“受保护”variables和代码执行的周围。 我们在通过堆栈进行函数调用时有严格的绑定(等待返回发生)。 所有这些都造成了像Erlang这样的无共享系统的瓶颈问题。
编辑:我也应该指出,Erlang是asynchronous的。 你发送你的消息,也许/有一天另一条消息回来。 或不。
斯宾塞关于无序执行的观点也很重要,并得到了很好的回答。
消息队列系统很酷,因为它有效地产生了“正在等待结果”的效果,这是你正在阅读的同步部分。 是什么让这个令人难以置信的令人难以置信的是,这意味着线路不需要顺序执行。 考虑下面的代码:
r = methodWithALotOfDiskProcessing(); x = r + 1; y = methodWithALotOfNetworkProcessing(); w = x * y
考虑一下,methodWithALotOfDiskProcessing()需要大约2秒才能完成,而methodWithALotOfNetworkProcessing()则需要大约1秒才能完成。 在程序语言中,这段代码需要大约3秒钟的时间才能运行,因为这些代码行将被顺序执行。 我们浪费时间等待一种方法完成,可以与另一种方法同时运行而不需要争夺单个资源。 在function语言中,代码行并不指示处理器何时尝试它们。 一个函数式语言会尝试如下所示:
Execute line 1 ... wait. Execute line 2 ... wait for r value. Execute line 3 ... wait. Execute line 4 ... wait for x and y value. Line 3 returned ... y value set, message line 4. Line 1 returned ... r value set, message line 2. Line 2 returned ... x value set, message line 4. Line 4 returned ... done.
多么酷啊? 通过继续使用代码,只需要等待,我们已经将等待时间自动减less到两秒钟! :D所以是的,虽然代码是同步的,但它往往与过程语言有不同的含义。
编辑:
一旦你把这个概念与Godeke的文章结合起来,很容易想象如何简单地利用多个处理器,服务器场,冗余数据存储以及谁知道还有什么。
这很可能是你与顺序混合同步 。
erlang中的函数体正在被顺序处理。 所以斯宾塞对这个“自动效应”所说的话并不适用于二郎。 你可以用erlang来build模这个行为。
例如,你可以产生一个计算一行中字数的过程。 正如我们有几行,我们为每一行产生一个这样的过程,并接收到答案来计算它的总和。
这样,我们产生了进行“重”计算的进程(利用额外的内核,如果可用的话),然后我们收集结果。
-module(countwords). -export([count_words_in_lines/1]). count_words_in_lines(Lines) -> % For each line in lines run spawn_summarizer with the process id (pid) % and a line to work on as arguments. % This is a list comprehension and spawn_summarizer will return the pid % of the process that was created. So the variable Pids will hold a list % of process ids. Pids = [spawn_summarizer(self(), Line) || Line <- Lines], % For each pid receive the answer. This will happen in the same order in % which the processes were created, because we saved [pid1, pid2, ...] in % the variable Pids and now we consume this list. Results = [receive_result(Pid) || Pid <- Pids], % Sum up the results. WordCount = lists:sum(Results), io:format("We've got ~p words, Sir!~n", [WordCount]). spawn_summarizer(S, Line) -> % Create a anonymous function and save it in the variable F. F = fun() -> % Split line into words. ListOfWords = string:tokens(Line, " "), Length = length(ListOfWords), io:format("process ~p calculated ~p words~n", [self(), Length]), % Send a tuple containing our pid and Length to S. S ! {self(), Length} end, % There is no return in erlang, instead the last value in a function is % returned implicitly. % Spawn the anonymous function and return the pid of the new process. spawn(F). % The Variable Pid gets bound in the function head. % In erlang, you can only assign to a variable once. receive_result(Pid) -> receive % Pattern-matching: the block behind "->" will execute only if we receive % a tuple that matches the one below. The variable Pid is already bound, % so we are waiting here for the answer of a specific process. % N is unbound so we accept any value. {Pid, N} -> io:format("Received \"~p\" from process ~p~n", [N, Pid]), N end.
这就是我们在shell中运行时的样子:
Eshell V5.6.5 (abort with ^G) 1> Lines = ["This is a string of text", "and this is another", "and yet another", "it's getting boring now"]. ["This is a string of text","and this is another", "and yet another","it's getting boring now"] 2> c(countwords). {ok,countwords} 3> countwords:count_words_in_lines(Lines). process <0.39.0> calculated 6 words process <0.40.0> calculated 4 words process <0.41.0> calculated 3 words process <0.42.0> calculated 4 words Received "6" from process <0.39.0> Received "4" from process <0.40.0> Received "3" from process <0.41.0> Received "4" from process <0.42.0> We've got 17 words, Sir! ok 4>
使Erlang扩展的关键在于并发性。
操作系统通过两种机制来提供并发性:
- 操作系统进程
- 操作系统线程
进程不共享状态 – 一个进程不能通过devise使另一个进程崩溃。
线程共享状态 – 一个线程可能会因devise而崩溃 – 这是您的问题。
使用Erlang–虚拟机使用一个操作系统进程,VM为Erlang程序提供并发性,而不是通过使用操作系统线程,而是通过提供Erlang进程 – 也就是Erlang实现自己的时间logging器。
这些Erlang进程通过发送消息(由Erlang VM而不是操作系统来处理)相互通信。 Erlang进程使用具有三部分地址<<N3.N2.N1>>
的进程ID(PID)相互地址:
- 不处理N1
- VM N2开启
- 物理机器N3
同一台虚拟机上的两个进程,同一台机器上的不同虚拟机或两台机器以相同的方式进行通信 – 因此,您的扩展与您部署应用程序的物理机数量(第一次近似)无关。
Erlang只是一个微不足道的线程安全 – 它没有线程。 (SMP /多核VM使用的语言是每个核心使用一个操作系统线程)。
您可能会误解Erlang的工作原理。 Erlang运行时最大限度地减less了CPU上的上下文切换,但是如果有多个可用的CPU,则全部用于处理消息。 在其他语言中,您没有“线程”,但可以同时处理大量的消息。
参考透明度:请参阅http://en.wikipedia.org/wiki/Referential_transparency_ (computer_science)
Erlang消息是纯粹的asynchronous的,如果你想同步回复你的消息,你需要明确的代码。 可能会说的是,进程消息框中的消息是按顺序处理的。 任何发送到进程的消息都会放在进程消息框中,进程从该进程中select一个消息来处理它,然后按照它认为合适的顺序继续进行下一个消息。 这是一个非常顺序的行为,接收块完全是这样做的。
看起来你已经混淆了同步和顺序克里斯提到。
在一个纯粹的函数式语言中,评估顺序并不重要 – 在一个函数应用程序fn(arg1,… argn)中,n个参数可以并行计算。 这保证了高水平的(自动)并行性。
Erlang使用一个进程modell,一个进程可以运行在同一个虚拟机上,或者在一个不同的处理器上运行 – 这是无法告诉的。 这是唯一可能的,因为消息在进程之间复制,没有共享(可变)的状态。 多处理器并列比multithreading要远得多,因为线程依赖于共享内存,在8核CPU上只能有8个线程并行运行,而多处理可以扩展到数千个并行进程。