第二十章 多核编程

20.1 如何在多核的CPU上更有效率的运行

20.1.1 使用大量进程

这个标准…显而易见。

20.1.2 避免副作用

因为存在副作用, 导致使用共享内存方式时必须使用锁机制, 虽然Erlang没有共享内存, 但对于可以被多个进程共享的ETS表和DETS表还是应该特别注意。

20.1.3 顺序瓶颈

对于本质就是顺序性的问题, 显然无法做到并发化。
而磁盘IO, 也是一个无法避免的自然瓶颈。
注册进程, 人为的创建了一个潜在的顺序瓶颈。

20.2 并行化顺序代码

并行化的map

pmap(F, L) ->
    S = self(),
    Ref = erlang:make_ref(),
    %% 对于列表中的每个参数都启动一个进程去处理 
    Pids = map(fun(I) ->spawn(fun() ->do_f(S, Ref, F, I) end) end, L),
    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([], _) ->[].

什么时候可以用pmap:1. 计算量很小的函数; 2. 不创建太多的进程; 3. 在恰当的抽象层次上思考

20.3 小消息, 大计算

启动SMP Erlang

# -smp  启动SMP Erlang
# +S N  使用N个Erlang虚拟机
$ erl -smp +S N
  测试不同的虚拟机数量对性能的影响
#!/bin/sh
echo "" >results
for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 
do
   echo $i
   erl -boot start_clean -noshell -smp +S $i -s ptests tests $i >> results
done

20.4 map-reduce算法和磁盘索引程序

20.4.1 map-reduce算法

%% map函数     MapReduce每次给列表中的每个X创建一个新的进程 
F1 = fun(Pid, X) ->void,

%% reduce函数  针对每个键值, 将它所对应的所有值合并到一起
%% Acc0       累加器
F2 = fun(key, [Value], Acc0) ->Acc
L = [X]
Acc = X = term()

%% 调用形式
mapreduce(F1, F2, Acc0, L) ->Acc

具体的实现

mapreduce(F1, F2, Acc0, L) ->
    S = self(),
    %% 启动新的进程运行reduce函数 
    Pid = spawn(fun() ->reduce(S, F1, F2, Acc0, L) end),
    receive
        {Pid, Result} ->
            Result
    end.


reduce(Parent, F1, F2, Acc0, L) ->
    process_flag(trap_exit, true),
    ReducePid = self(),

    %% map过程的实现
    %% 对于列表中的每个值都启动一个进程在do_job中调用F1进行处理 
    foreach(fun(X) ->
            spawn_link(fun() ->do_job(ReducePid, F1, X) end)
        end, L),
    N = length(L),
    %% 用字典存储键值
    Dict0 = dict:new(),
    %% 等待map过程完成
    Dict1 = collect_replies(N, Dict0),
    %% 调用F2按相同键值进行合并 
    Acc = dict:fold(F2, Acc0, Dict1),
    %% 向MapReduce进程通知运行结果
    Parent ! {self(), Acc}.

%% 按键值进行合并的过程
collect_replies(0, Dict) ->
    Dict;
collect_replies(N, Dict) ->
    receive
        %% 对键-值的处理
        %% 存在Key则将Val相加, 否则插入到字典
        {Key, Val} ->
            case dict:is_key(Key, Dict) of
                true ->
                    Dict1 = dict:append(Key, Val, Dict),
                    collect_replies(N, Dict1);
                false ->
                    Dict1 = dict:store(Key,[Val], Dict),
                    collect_replies(N, Dict1)
            end;
        {'EXIT', _,  _Why} ->
            collect_replies(N-1, Dict)
    end.

%% 执行指定的map函数
do_job(ReducePid, F, X) ->
    F(ReducePid, X).

测试代码:

-module(test_mapreduce).
-compile(export_all).
-import(lists, [reverse/1, sort/1]).

test() ->
    wc_dir(".").

wc_dir(Dir) ->
    %% map函数
    F1 = fun generate_words/2,
    %% reduce函数
    F2 = fun count_words/3,
    %% 参数列表
    Files = lib_find:files(Dir, ".*[.](erl)", false),
    %% 调用mapreduce处理
    L1 = phofs:mapreduce(F1, F2, [], Files),
    reverse(sort(L1)).

%% 查找文件中的每个单词
generate_words(Pid, File) ->
    F = fun(Word) ->Pid ! {Word, 1} end,
    lib_misc:foreachWordInFile(File, F).

%% 统计有多少个不同的单词
count_words(Key, Vals, A) ->
    [{length(Vals), Key}|A].

运行结果:

1> test_mapreduce:test().
[{115,"L"},
 {84,"T"},
 {80,"1"},
 {77,"end"},
 {72,"X"},
 {52,"H"},
 {47,"file"},
 {46,"S"},
 {44,"of"},
 {43,"F"},
 {40,"2"},
 {39,"Key"},
 {39,"Fun"},
 {37,"is"},
 {35,"case"},
 {34,"fun"},
 {34,"Pid"},
 {34,"N"},
 {33,"File"},
 {32,"true"},
 {31,"Str"},
 {28,"ok"},
 {27,"prefix"},
 {27,"Val"},
 {27,"I"},
 {26,"to"},
 {26,[...]},
 {24,...},
 {...}|...]

20.4.2 全文检索

    1. 反向索引
      文件-内容对照表
文件名 内容
/home/dogs rover jack buster winston
/home/animals/cats zorro daisy jaguar
/home/cars rover jaguar ford

索引-文件对照表

索引 文件名
1 /home/dogs
2 /home/animals/cats
3 /home/cars

单词-索引对照表

单词 索引
rover 1,3
jack 1
buster 1
winston 1
zorro 2
daisy 2
jaguar 2,3
ford 3
    1. 反向索引的查询
      通过单词-索引, 索引-文件的对照表查找单词与文件的对应关系
    1. 反向索引的数据结构
      因为一个常见的词可能在成千上万的文件中出现, 因此使用数字索引代替文件名可大大节省存储空间, 因此需要文件与索引的对照表。
      对于每个在文件中出现的单词, 都需要记录此文件的索引号, 因此建立单词与索引的对照表。

20.4.3 索引器的操作

%% 启动一个名为indexer_server的服务器进程
%% 启动一个worker进程来执行索引动作 
start() ->
    indexer_server:start(output_dir()),
    spawn_link(fun() ->worker() end).

worker() ->
    possibly_stop(),
    %% 返回下一个需要索引的目录
    case indexer_server:next_dir() of
    {ok, Dir} ->
        %% 查找目录下需要进行索引的文件 
        Files = indexer_misc:files_in_dir(Dir),
        %% 为其建立索引
        index_these_files(Files),
        %% 检测是否正常完成
        indexer_server:checkpoint(),
        possibly_stop(),
        sleep(10000),
        worker();
    done ->
        true
    end.

%% 使用MapReduce算法实现建立索引的并行处理 
index_these_files(Files) ->
    Ets = indexer_server:ets_table(),
    OutDir = filename:join(indexer_server:outdir(), "index"),
    %% map函数
    F1 = fun(Pid, File) ->indexer_words:words_in_file(Pid, File, Ets) end,
    %% reduce函数
    F2 = fun(Key, Val, Acc) ->handle_result(Key, Val, OutDir, Acc) end,
    indexer_misc:mapreduce(F1, F2, 0, Files).

%% 按照Key值进行合并
handle_result(Key, Vals, OutDir, Acc) ->
    add_to_file(OutDir, Key, Vals),
    Acc + 1.

%% 将索引数组添加到Word中
add_to_file(OutDir, Word, Is) ->
    L1 = map(fun(I) -><<I:32>> end, Is),
    OutFile = filename:join(OutDir, Word),
    case file:open(OutFile, [write,binary,raw,append]) of
    {ok, S} ->
        file:pwrite(S, 0, L1),
        file:close(S);
    {error, E} ->
          exit({ebadFileOp, OutFile, E})
    end.

20.4.4 运行索引器

1> indexer:cold_start().
2> indexer:start().
3> indexer:stop().

20.4.5 评论

可以改进的三个方面

1. 改进单词抽取
2. 改进map-reduce算法, 以便处理海量数据
3. 方向索引的数据结构只使用了文件系统来存储

20.4.6 索引器的代码

posted @ 2020-08-24 11:24  养诚  阅读(188)  评论(0编辑  收藏  举报