函数语言(特别是Erlang)如何/为什么很好地缩放?


92

一段时间以来,我一直在关注功能编程语言和功能的日益普及。我调查了他们,却没有看到上诉的原因。

然后,最近我在Codemash上参加了Kevin Smith的“ Erlang基础”演讲。

我享受了演示,并了解到函数式编程的许多属性使避免线程/并发问题变得更加容易。我知道状态和可变性的缺乏使多个线程无法更改同一数据,但是Kevin表示(如果我理解正确的话),所有通信都是通过消息进行的,消息是同步处理的(再次避免了并发问题)。

但是我读过Erlang用于高度可扩展的应用程序(这是爱立信首先创建它的全部原因)。如果将所有内容都作为同步处理的消息来处理,那么如何高效地每秒处理数千个请求?这不是为什么我们开始转向异步处理-这样我们才能利用同时运行多个操作线程并实现可伸缩性的优势吗?看起来这种体系结构虽然更安全,但在可伸缩性方面却倒退了一步。我想念什么?

我了解Erlang的创建者有意避免支持线程以避免并发问题,但是我认为多线程是实现可伸缩性所必需的。

函数式编程语言如何才能固有地具有线程安全性,又可以扩展?


1
[未提及]:Erlangs的VM将异步性提高到另一个层次。通过voodoo magic(asm),它可以在不停止os线程的情况下阻止socket:read之类的同步操作。当其他语言迫使您进入异步回调嵌套时,这使您可以编写同步代码。编写扩展应用程序要比单线程微服务的思维图要容易得多,因为每次将代码添加到代码库时都要牢记全局。
Vans S

@范S有趣。
吉姆·安德森

Answers:


97

通常,功能语言不依赖于变量的变异。因此,由于值是固定的,因此我们不必保护变量的“共享状态”。反过来,这避免了传统语言在处理器或机器上实现算法所必须经历的大多数麻烦。

通过在消息传递系统中进行烘焙,Erlang使它比传统功能语言更进一步,该系统使所有内容都可以在基于事件的系统上运行,在该系统中,一段代码仅担心接收消息和发送消息,而不用担心更大的局面。

这意味着程序员(名义上)不担心该消息将在另一个处理器或机器上处理:仅发送该消息就足以使其继续。如果它关心响应,它将作为另一个消息等待它。

最终结果是每个代码段都独立于其他所有代码段。没有共享的代码,没有共享的状态,并且所有来自消息系统的交互都可以在许多硬件之间分配(或不分配)。

与传统系统相反:我们必须在“受保护”变量和代码执行周围放置互斥体和信号量。我们通过堆栈在函数调用中有严格的绑定(等待返回发生)。所有这些都造成了瓶颈,而这些瓶颈在像Erlang这样的“无共享”系统中问题不大。

编辑:我还应该指出,Erlang是异步的。您发送了一条消息,也许/有一天另一条消息又回来了。或不。

Spencer关于乱序执行的观点也很重要,并且得到了很好的回答。


我了解这一点,但看不到消息模型的效率如何。我想相反。这真让我大开眼界。难怪功能编程语言得到了如此多的关注。
Jim Anderson

3
在无共享系统中,您将获得很多并发潜力。一个不好的实现(例如,高消息传递开销)可能会破坏这一点,但是Erlang似乎做到了正确,并使所有内容保持轻量化。
Godeke,2009年

重要的是要注意,尽管Erlang具有消息传递语义,但它具有共享的内存实现,因此,它具有所描述的语义,但是如果不是必须的话,它不会全部复制所有内容。
亚伦·曼帕

1
@Godeke:“ Erlang(像大多数功能语言一样)在可能的情况下保留任何数据的单个实例”。在AFAIK中,由于缺乏并发GC,Erlang实际上深度复制了其轻量级进程之间传递的所有内容。
JD

1
@JonHarrop几乎是正确的:当一个进程向另一个进程发送消息时,消息被复制;大型二进制文件除外,这些二进制文件通过引用传递。请参阅jlouisramblings.blogspot.hu/2013/10/embrace-copying.html以了解这是一件好事的原因。
hcs42

73

消息队列系统很酷,因为它有效地产生了“触发结果并等待结果”的效果,这是您正在阅读的同步部分。之所以如此令人称奇,是因为它意味着不需要按顺序执行行。考虑以下代码:

r = methodWithALotOfDiskProcessing();
x = r + 1;
y = methodWithALotOfNetworkProcessing();
w = x * y

考虑一下,methodWithALotOfDiskProcessing()大约需要2秒才能完成,而methodWithALotOfNetworkProcessing()大约需要1秒才能完成。在程序语言中,此代码将花费大约3秒钟的时间运行,因为这些行将按顺序执行。我们在浪费时间等待一种方法可以完成,而另一种方法可以与另一种方法同时运行,而不用竞争一种资源。在功能语言中,代码行不指示处理器何时尝试它们。一种功能语言会尝试以下操作:

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.

多么酷啊?通过继续执行代码并仅在需要的地方等待,我们将等待时间自动减少到了两秒!:D所以是的,虽然代码是同步的,但它的含义往往与过程语言中的含义不同。

编辑:

一旦您将这一概念与Godeke的帖子结合使用,就可以轻松想象利用多个处理器,服务器场,冗余数据存储以及其他人知道这些变得多么简单


凉!我完全误解了消息的处理方式。谢谢,您的帖子会有所帮助。
Jim Anderson

“一种功能性语言会尝试以下操作”-我不确定其他功能性语言,但是在Erlang中,该示例将完全像过程性语言一样工作。您可以通过生成进程来并行执行这两个任务,让它们异步执行这两个任务,最后获得它们的结果,但这不像“代码同步时,它的含义往往不同于过程语言。” ” 另请参阅克里斯的答案。
hcs42

16

您可能会混淆了跟顺序同步

erlang中的函数主体正在按顺序处理。因此,斯宾塞关于“自动效应”的说法并不适用于埃尔朗。您可以使用erlang对这种行为进行建模。

例如,您可以生成一个计算一行中的单词数的进程。当我们有多行时,我们为每行生成一个这样的过程,并接收答案以从中计算总和。

这样,我们生成了进行“大量”计算的流程(如果可用,则利用其他核),然后收集结果。

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

13

使Erlang扩展的关键之处与并发性有关。

操作系统通过两种机制提供并发:

  • 操作系统进程
  • 操作系统线程

进程不共享状态–设计不会使一个进程崩溃。

线程共享状态–一个线程可以通过设计使另一个线程崩溃–这就是您的问题。

使用Erlang –虚拟机使用一个操作系统进程,并且VM不通过使用操作系统线程而是通过提供Erlang进程来为Erlang程序提供并发性-即Erlang实现了自己的时间分配器。

这些Erlang进程通过发送消息(由Erlang VM而非操作系统处理)相互交谈。Erlang进程使用具有三部分地址的进程ID(PID)相互寻址<<N3.N2.N1>>

  • 否处理N1
  • VM N2开启
  • 物理机N3

同一台VM上,同一台计算机上的不同VM上或两台计算机上的两个VM上的两个进程以相同的方式进行通信–因此,扩展规模与部署应用程序的物理计算机数量无关(在第一近似中)。

Erlang仅在琐碎的意义上是线程安全的–它没有线程。(即SMP /多核VM的语言每个核使用一个操作系统线程)。


7

您可能对Erlang的工作方式有误解。Erlang运行时最大程度地减少了CPU上的上下文切换,但是如果有多个CPU可用,则全部用于处理消息。从某种意义上讲,您没有“线程”,但是可以同时处理许多消息。


4

Erlang消息是纯异步的,如果您想对消息进行同步回复,则需要为此明确编写代码。可能所说的是过程消息框中的消息是顺序处理的。发送到流程的所有消息都位于该流程消息框中,该流程从该框中选择一条消息进行处理,然后按其认为合适的顺序移至下一条消息。这是一个非常顺序的动作,而接收块正是这样做的。

就像您提到的chris一样,您混合了同步和顺序。



-2

在纯函数式语言中,计算顺序无关紧要-在函数应用程序fn(arg1,.. argn)中,可以并行计算n个参数。这保证了高水平的(自动)并行性。

Erlang使用流程模型,其中一个流程可以在相同的虚拟机或不同的处理器上运行-无法分辨。这仅是可能的,因为消息是在进程之间复制的,没有共享(可变)状态。多处理器并行处理比多线程处理要复杂得多,因为线程依赖共享内存,因此在8核CPU上只有8个线程并行运行,而多处理则可以扩展到数千个并行进程。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.