92

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

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

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

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

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

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

4

8 回答 8

100

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

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

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

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

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

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

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

于 2009-01-23T21:07:16.127 回答
74

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

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 的帖子掌握了这个概念,就很容易想象利用多个处理器、服务器群、冗余数据存储以及其他方面的优势是多么简单。

于 2009-01-23T21:18:27.040 回答
16

您很可能将synchronizesequence 混为一谈

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> 
于 2009-02-05T06:25:22.390 回答
13

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

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

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

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

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

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

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

  • 进程号 N1 on
  • 虚拟机 N2 开启
  • 物理机N3

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

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

于 2009-01-24T14:24:15.800 回答
7

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

于 2009-01-23T21:12:58.627 回答
4

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

看起来你已经混合了克里斯提到的同步和顺序。

于 2009-02-10T17:29:41.843 回答
3

参考透明度:见http://en.wikipedia.org/wiki/Referential_transparency_(computer_science)

于 2009-01-23T21:07:51.507 回答
-2

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

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

于 2009-01-23T21:20:11.110 回答