功能语言(特别是Erlang)如何/为何能够很好地扩展?

时间:2009-01-23 20:59:29

标签: concurrency functional-programming erlang scalability

我一直在关注函数式编程语言和功能的日益增长的可见性。我调查了他们,没有看到上诉的原因。

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

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

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

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

函数式编程语言如何具有固有的线程安全性,但仍然可以扩展?

8 个答案:

答案 0 :(得分:92)

功能语言(通常)不依赖于变异变量。因此,我们不必保护变量的“共享状态”,因为该值是固定的。这反过来避免了传统语言必须通过处理器或机器实现算法的大部分环箍跳跃。

Erlang通过在消息传递系统中烘焙使得它比传统的功能语言更进一步,它允许所有内容在基于事件的系统上运行,其中一段代码只担心接收消息和发送消息,而不用担心更大的图片。

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

这样做的最终结果是每个代码段都与其他代码段无关。没有共享代码,没有共享状态以及来自消息系统的所有交互,这些消息系统可以分布在多个硬件之间(或不是)。

将此与传统系统进行对比:我们必须在“受保护”变量和代码执行周围放置互斥锁和信号量。我们通过堆栈在函数调用中进行紧密绑定(等待返回发生)。所有这些都会产生瓶颈,这在像Erlang这样的无共享系统中不会成为问题。

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

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

答案 1 :(得分:71)

消息队列系统很酷,因为它有效地产生了“火 - 等待结果”效果,这是您正在阅读的同步部分。令人难以置信的令人惊叹的是它意味着不需要按顺序执行行。请考虑以下代码:

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的帖子掌握了这个概念,很容易想象如何利用多个处理器,服务器群,冗余数据存储以及谁知道还有什么呢?

>

答案 2 :(得分:15)

您可能会将同步顺序混合在一起。

正在按顺序处理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> 

答案 3 :(得分:12)

使Erlang能够扩展的关键是与并发性有关。

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

  • 操作系统流程
  • 操作系统线程

进程不共享状态 - 一个进程不能通过设计崩溃另一个进程。

线程共享状态 - 一个线程可能会因设计而崩溃 - 这就是你的问题。

使用Erlang - 虚拟机使用一个操作系统进程,VM不向Erlang程序提供并发,而不是通过使用操作系统线程,而是提供Erlang进程 - 即Erlang实现自己的时间段。

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

  • 上处理否N1
  • VM N2
  • 物理机器N3

同一台虚拟机上的两个进程,在同一台机器上的不同虚拟机或两台机器上以相同的方式进行通信 - 因此,您的扩展与您部署应用程序的物理机数量无关(在第一次近似中)。 / p>

Erlang只是一个微不足道的线程安全 - 它没有线程。 (语言即SMP /多核VM每个核心使用一个操作系统线程)。

答案 4 :(得分:6)

您可能对Erlang如何工作有误解。 Erlang运行时最小化CPU上下文切换,但如果有多个CPU可用,则所有CPU都用于处理消息。您在其他语言中没有“线程”,但是您可以同时处理大量消息。

答案 5 :(得分:3)

Erlang消息完全是异步的,如果您想要对消息进行同步回复,则需要为此明确编写代码。可能的说法是按顺序处理进程消息框中的消息。发送给进程的任何消息都位于该进程消息框中,进程从该框中选择一条消息进行处理,然后按照它认为合适的顺序转到下一条消息。这是一个非常顺序的行为,接收块就是这样做的。

看起来像克里斯提到的那样混淆了同步和顺序。

答案 6 :(得分:2)

答案 7 :(得分:-1)

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

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