【问题标题】:How/why do functional languages (specifically Erlang) scale well?函数式语言(特别是 Erlang)如何/为什么能够很好地扩展?
【发布时间】:2010-10-03 05:39:03
【问题描述】:

一段时间以来,我一直在关注函数式编程语言和功能日益增长的知名度。我调查了他们,没有看到上诉的原因。

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

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

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

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

函数式编程语言如何在本质上是线程安全的,但仍可扩展?

【问题讨论】:

  • [未提及]:Erlangs 的 VM 将异步性提升到了另一个层次。通过巫术魔法 (asm),它允许像 socket:read 这样的同步操作在不停止操作系统线程的情况下阻塞。这允许您在其他语言强制您进入异步回调嵌套时编写同步代码。使用单线程微服务的思维图编写扩展应用程序要容易得多,而每次在代码库中添加一些东西时都要牢记大局。
  • @Vans S 有趣。

标签: concurrency functional-programming erlang scalability


【解决方案1】:

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

Erlang 比传统的函数式语言更进一步,它在消息传递系统中进行了烘焙,该系统允许一切都在基于事件的系统上运行,其中一段代码只关心接收消息和发送消息,而不关心更大的图景。

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

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

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

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

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

【讨论】:

  • 这个我明白,但是没看出消息模型的效率如何。我猜相反。这让我大开眼界。难怪函数式编程语言受到如此多的关注。
  • 您在无共享系统中获得大量并发潜力。一个糟糕的实现(例如,高消息传递开销)可能会破坏这一点,但 Erlang 似乎做得对并保持一切轻量级。
  • 重要的是要注意,虽然 Erlang 具有消息传递语义,但它具有共享内存实现,因此,它具有所描述的语义,但如果没有,它不会在整个地方复制内容必须。
  • @Godeke:“Erlang(像大多数函数式语言一样)尽可能保留任何数据的单个实例”。 AFAIK,由于缺乏并发 GC,Erlang 实际上深度复制了在其轻量级进程之间传递的所有内容。
  • @JonHarrop 几乎是对的:当一个进程向另一个进程发送消息时,该消息被复制;除了通过引用传递的大型二进制文件。参见例如jlouisramblings.blogspot.hu/2013/10/embrace-copying.html 为什么这是一件好事。
【解决方案2】:

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

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

【讨论】:

  • 酷!我完全误解了消息是如何处理的。谢谢,你的帖子有帮助。
  • “函数式语言会尝试类似下面的东西” - 我不确定其他函数式语言,但在 Erlang 中,该示例的工作方式与过程语言的情况完全相同。您可以通过生成进程并行执行这两个任务,让它们异步执行这两个任务,并在最后获得它们的结果,但这不像“虽然代码是同步的,但它往往有一个与程序语言不同的含义。”另请参阅 Chris 的回答。
【解决方案3】:

您可能将同步顺序混为一谈。

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

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

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

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

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

    线程共享状态 - 一个线程可以按设计使另一个线程崩溃 - 这是你的问题。

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

    这些 Erlang 进程通过发送消息(由 Erlang VM 而非操作系统处理)相互通信。 Erlang 进程使用进程 ID (PID) 相互寻址,该进程 ID (PID) 具有三部分地址 &lt;&lt;N3.N2.N1&gt;&gt;

    • 进程号 N1 开启
    • VM N2 开启
    • 物理机N3

    同一台虚拟机上的两个进程、同一台机器上的不同虚拟机上的两个进程或两台机器以相同的方式进行通信——因此,您的扩展与您部署应用程序的物理机器的数量无关(初步近似)。

    Erlang 只是简单意义上的线程安全——它没有线程。 (即 SMP/多核 VM 的语言每个内核使用一个操作系统线程)。

    【讨论】:

      【解决方案5】:

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

      【讨论】:

        【解决方案6】:

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

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

        【讨论】:

          【解决方案7】:
          【解决方案8】:

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

          Erlang 使用一个进程模型,其中一个进程可以在同一个虚拟机上运行,​​或者在不同的处理器上运行——没有办法分辨。这是可能的,因为消息是在进程之间复制的,没有共享(可变)状态。多处理器并行比多线程走得更远,因为线程依赖于共享内存,所以在 8 核 CPU 上只能有 8 个线程并行运行,而多处理可以扩展到数千个并行进程。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2018-06-21
            • 2011-03-11
            • 1970-01-01
            • 1970-01-01
            • 2013-04-05
            • 2010-11-28
            • 2011-04-01
            相关资源
            最近更新 更多