代码之家  ›  专栏  ›  技术社区  ›  Jim Anderson

函数式语言(特别是Erlang)如何/为什么能够很好地扩展?

  •  86
  • Jim Anderson  · 技术社区  · 17 年前

    一段时间以来,我一直在关注函数式编程语言和功能的可见性不断提高。我调查了一下,没有发现上诉的原因。

    然后,最近我参加了Kevin Smith在 Codemash .

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

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

    我知道Erlang的创建者故意避免支持线程以避免并发问题,但我认为多线程是实现可伸缩性的必要条件。

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

    8 回复  |  直到 17 年前
        1
  •  99
  •   2240    6 年前

    函数式语言(通常)不依赖于 mutating 一个变量。因此,我们不必保护变量的“共享状态”,因为该值是固定的。这反过来又避免了传统语言在跨处理器或机器实现算法时必须经历的大部分困难。

    Erlang比传统的函数式语言更进一步,它在一个消息传递系统中烘焙,该系统允许在一个基于事件的系统上操作所有东西,在这个系统中,一段代码只关心接收消息和发送消息,而不关心全局。

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

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

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

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

    斯宾塞关于无序执行的观点也很重要,并且得到了很好的回答。

        2
  •  74
  •   Spencer Ruport    16 年前

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

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

    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.
    

    那有多酷?通过继续执行代码并仅在必要时等待,我们自动将等待时间减少到了2秒!:因此,是的,虽然代码是同步的,但它的含义往往与过程语言不同。

    编辑:

    一旦你结合Godeke的帖子理解了这个概念,你就很容易想象 易于理解的

        3
  •  16
  •   Chris Czura    17 年前

    很可能你把事情搞混了 同步的 顺序的 .

    erlang中的函数体正在按顺序处理。 所以斯宾塞所说的“自动效应”对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> 
    
        4
  •  13
  •   hcs42    10 年前

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

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

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

    进程不共享状态一个进程不能按设计使另一个进程崩溃。

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

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

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

    • 第N1号过程开启
    • VM N2开启
    • 物理机器N3

    同一个VM上的两个进程、同一台机器上的不同VM上的两个进程或两台机器以相同的方式通信–因此,您的扩展与您部署应用程序的物理机器的数量无关(在第一种近似情况下)。

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

        5
  •  7
  •   Kristopher Johnson    17 年前

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

        6
  •  4
  •   Jebu    17 年前

    Erlang消息是纯异步的,如果您希望同步回复消息,则需要显式地为此编写代码。可能说的是,流程消息框中的消息是按顺序处理的。发送到流程的任何消息都会位于该流程消息框中,流程将从该框中选择一条消息并对其进行处理,然后按照其认为合适的顺序移动到下一条消息。这是一个非常连续的动作,接收块正是这样做的。

    看起来你像chris提到的那样混淆了同步和顺序。

        8
  •  -2
  •   mfx    17 年前

    推荐文章