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

Concurrency 函数式语言(特别是Erlang)如何/为什么能够很好地扩展?,concurrency,functional-programming,erlang,scalability,Concurrency,Functional Programming,Erlang,Scalability,一段时间以来,我一直在关注函数式编程语言和功能的可见性不断提高。我调查了一下,没有发现上诉的原因 然后,最近我参加了Kevin Smith的“Erlang基础知识”演讲 我喜欢这个演示,并了解到函数式编程的许多属性使避免线程/并发问题变得更容易。我理解缺乏状态和易变性使得多个线程不可能更改相同的数据,但是Kevin说(如果我理解正确的话)所有通信都是通过消息进行的,并且mesages是同步处理的(同样避免了并发问题) 但我已经读到,Erlang用于高度可伸缩的应用程序(这就是爱立信最初创建它的全

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

然后,最近我参加了Kevin Smith的“Erlang基础知识”演讲

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

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

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


函数式编程语言如何在本质上是线程安全的,但仍然可以扩展?函数式语言(通常)不依赖于变量。因此,我们不必保护变量的“共享状态”,因为该值是固定的。这反过来又避免了传统语言在跨处理器或机器实现算法时必须经历的大部分困难

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

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

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

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

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


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

参考透明性:请参见

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

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

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

编辑:


一旦你结合Godeke的文章理解了这个概念,就很容易想象利用多个处理器、服务器场、冗余数据存储和谁知道还有什么变得多么简单。

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


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

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