在编程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
如何避免这个瓶颈?
答案 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。
一般来说,你想做三件事:
选择一个“足够大”的工作单位。如果工作单元太小,则会因处理开销而死亡。如果它太大,你就会因工人闲置而死亡,特别是如果你的工作没有被列表中的元素数量平均分配。
上限同时发生的工人数量。 Psyeugenic建议通过调度程序将其拆分,我建议按工作计数限制将其拆分,100个工作说。也就是说,你想要开始100个工作,然后等到其中一些工作完成,然后再开始更多的工作。
如果可能,请考虑拧紧元素的顺序。如果您不需要考虑订单,速度会快得多。对于许多问题,这是可能的。如果订单确实重要,那么使用dict
按照建议存储内容。大元素列表更快。
基本规则是,只要您想要并行,就很少需要基于列表的数据表示。该列表具有固有的线性,您不需要它。 Guy Steele就这个问题进行了一次演讲:http://vimeo.com/6624203
答案 2 :(得分:1)
在这种情况下,您可以使用dict
(从生成的流程的pid到原始列表中的索引)改为Pids
。