如何在Erlang中优化数千条消息的接收循环?

时间:2011-09-29 09:20:39

标签: erlang parallel-processing

在编程Erlang书的“编程多核CPU”一章中,Joe Armstrong给出了一个很好的并行化地图函数的例子:

pmap(F, L) ->
    S = self(),
    %% make_ref() returns a unique reference
    %% we'll match on this later
    Ref = erlang:make_ref(),
    Pids = map(fun(I) ->
        spawn(fun() -> do_f(S, Ref, F, I) end)
    end, L),
    %% gather the results
    gather(Pids, Ref).

do_f(Parent, Ref, F, I) ->
    Parent ! {self(), Ref, (catch F(I))}.

gather([Pid|T], Ref) ->
    receive
        {Pid, Ref, Ret} -> [Ret|gather(T, Ref)]
    end;

gather([], _) ->
    [].

它工作得很好,但我相信它存在一个瓶颈,导致它在包含100,000多个元素的列表上工作得非常慢。

执行gather()函数时,它会开始匹配Pid列表中的第一个Pids与主进程邮箱中的消息。但是,如果邮箱中最旧的邮件不是来自Pid,那怎么办?然后它会尝试所有其他消息,直到找到匹配项。话虽如此,有一定的可能性,在执行gather()函数时,我们必须循环遍历所有邮箱消息,以找到我们从{{1}获取的Pid的匹配项}列表。对于大小为N的列表,这是N * N最坏情况。

我甚至设法证明了这个瓶颈的存在:

Pids

如何避免这个瓶颈?

3 个答案:

答案 0 :(得分:3)

问题是如果你想要一个正确的解决方案,你仍然需要:

  • 检查给定的回复是否来自您拥有的某个进程 产生了
  • 确保正确的结果顺序

这是一个使用计数器而不是列表的解决方案 - 这消除了多次遍历收件箱的必要性。匹配Ref可确保我们收到的消息来自我们的孩子。通过在lists:keysort/2的最后使用pmap对结果进行排序来确保正确的顺序,这会增加一些开销,但可能会小于O(n^2)

-module(test).

-compile(export_all).

pmap(F, L) ->
    S = self(),
    % make_ref() returns a unique reference
    % we'll match on this later
    Ref = erlang:make_ref(),
    Count = lists:foldl(fun(I, C) ->
                                spawn(fun() ->
                                              do_f(C, S, Ref, F, I)
                                      end),
                                C+1
                        end, 0, L),
    % gather the results
    Res = gather(0, Count, Ref),
    % reorder the results
    element(2, lists:unzip(lists:keysort(1, Res))).


do_f(C, Parent, Ref, F, I) ->
    Parent ! {C, Ref, (catch F(I))}.


gather(C, C, _) ->
    [];
gather(C, Count, Ref) ->
    receive
        {C, Ref, Ret} -> [{C, Ret}|gather(C+1, Count, Ref)]
    end.

答案 1 :(得分:2)

乔的例子很整洁,但在实践中你需要一个更重量级的解决方案来解决你的问题。例如,请查看http://code.google.com/p/plists/source/browse/trunk/src/plists.erl

一般来说,你想做三件事:

  1. 选择一个“足够大”的工作单位。如果工作单元太小,则会因处理开销而死亡。如果它太大,你就会因工人闲置而死亡,特别是如果你的工作没有被列表中的元素数量平均分配。

  2. 上限同时发生的工人数量。 Psyeugenic建议通过调度程序将其拆分,我建议按工作计数限制将其拆分,100个工作说。也就是说,你想要开始100个工作,然后等到其中一些工作完成,然后再开始更多的工作。

  3. 如果可能,请考虑拧紧元素的顺序。如果您不需要考虑订单,速度会快得多。对于许多问题,这是可能的。如果订单确实重要,那么使用dict按照建议存储内容。大元素列表更快。

  4. 基本规则是,只要您想要并行,就很少需要基于列表的数据表示。该列表具有固有的线性,您不需要它。 Guy Steele就这个问题进行了一次演讲:http://vimeo.com/6624203

答案 2 :(得分:1)

在这种情况下,您可以使用dict(从生成的流程的pid到原始列表中的索引)改为Pids