26、Erlang编程:文件系统组织、多核编程与并行化策略

Erlang编程:文件系统组织、多核编程与并行化策略

在当今的编程领域,高效利用多核CPU以及合理组织文件系统是提升程序性能和可维护性的关键。本文将深入探讨Erlang编程中文件系统的组织方式、多核编程的相关知识以及如何将顺序代码并行化。

1. 文件系统组织

在开发中,良好的文件系统组织有助于提高代码的可维护性和可读性。虽然这并非强制要求,但将应用不同部分的文件放在明确的位置是一种值得推荐的做法。以下是一个示例应用 sellaprime 所使用的主要文件及其内容:
| 文件 | 内容 |
| — | — |
| area_server.erl | 区域服务器 - gen_server 回调 |
| prime_server.erl | 素数服务器 - gen_server 回调 |
| sellaprim_supervisor.erl | 监督者回调 |
| sellaprim_app.erl | 应用回调 |
| my_alam_handler.erl | gen_event 的事件回调 |
| sellaprime.app | 应用规范 |
| elog4.config | 错误日志配置文件 |

启动 sellaprime 应用的步骤如下:

graph LR
    A[启动系统] --> B[查找sellaprime.app中的{mod, ...}声明]
    B --> C[调用sellaprime_app:start/2]
    C --> D[调用sellaprim_supervisor:start_link/2启动监督者]
    D --> E[调用sellaprim_supervisor:init/1安装错误处理程序并返回监督规范]
    E --> F[启动区域服务器和素数服务器]

具体操作步骤为:
1. 使用以下命令启动系统:

$ erl -boot start_sasl -config elog4.config
1> application:start(sellaprime).
  1. sellaprime.app 文件必须位于Erlang启动的根目录或其子目录中。应用控制器会在该文件中查找 {mod, ...} 声明,其中包含应用控制器的名称,这里是 sellaprime_app 模块。
  2. 调用回调例程 sellaprime_app:start/2
  3. sellaprime_app:start/2 调用 sellaprim_supervisor:start_link/2 ,启动 sellaprime 监督者。
  4. 调用监督者回调 sellaprim_supervisor:init/1 ,安装错误处理程序并返回监督规范,该规范说明了如何启动区域服务器和素数服务器。
  5. sellaprime 监督者启动区域服务器和素数服务器,它们都作为 gen_server 回调模块实现。

停止应用也很简单,只需调用 application:stop(sellaprime) init:stop()

2. 应用监控器

应用监控器是一个用于查看应用的图形用户界面(GUI)。使用 appmon:start() 命令可以启动应用查看器,启动后会出现一个窗口,点击其中的应用即可查看详细信息。

3. 素数生成

在某些应用中,可能需要生成素数。以下是生成素数的代码:

%% make a prime with at least K decimal digits.
%% Here we use 'Bertrand's postulate.
%% Bertrands postulate is that for every N > 3,
%% there is a prime P satisfying N < P < 2N - 2
%% This was proved by Tchebychef in 1850
%% (Erdos improved this proof in 1932)
make_prime(1) ->
    lists:nth(random:uniform(5), [1,2,3,5,7]);
make_prime(K) when K > 0 ->
    new_seed(),
    N = make_random_int(K),
    if N > 3 ->
        io:format("Generating a ~w digit prime ",[K]),
        MaxTries = N - 3,
        P1 = make_prime(MaxTries, N+1),
        io:format("~n",[]),
        P1;
    true ->
        make_prime(K)
    end.

make_prime(0, _) ->
    exit(impossible);
make_prime(K, P) ->
    io:format(".",[]),
    case is_prime(P) of
        true -> P;
        false -> make_prime(K-1, P+1)
    end.

%% Fermat's little theorem says that if
%% N is a prime and if A < N then
%% A^N mod N = A
is_prime(D) ->
    new_seed(),
    is_prime(D, 100).

is_prime(D, Ntests) ->
    N = length(integer_to_list(D)) -1,
    is_prime(Ntests, D, N).

is_prime(0, _, _) -> true;
is_prime(Ntest, N, Len) ->
    K = random:uniform(Len),
    %% A is a random number less than N
    A = make_random_int(K),
    if
        A < N ->
            case lib_lin:pow(A,N,N) of
                A -> is_prime(Ntest-1,N,Len);
                _ -> false
            end;
        true ->
            is_prime(Ntest, N, Len)
    end.

这段代码利用了Bertrand假设和Fermat小定理来生成指定位数的素数。

4. 多核编程基础

在多核CPU时代,如何编写能充分利用多核性能的程序是一个重要问题。过去有两种并发模型:共享状态并发和消息传递并发。大多数编程语言采用了共享状态并发,而Erlang社区则选择了消息传递并发。

共享状态并发涉及“可变状态”的概念,当多个进程共享和修改同一内存时,可能会导致灾难。为了保护共享内存,通常会使用锁机制,但这会带来诸多问题,如程序崩溃时其他程序的处理问题以及内存损坏问题。

相比之下,Erlang没有可变数据结构,这意味着无需使用锁,并且易于并行化。通过将问题的解决方案分解为多个并行进程,就可以实现并行化,这种编程风格被称为面向并发编程。

5. 让程序在多核CPU上高效运行的方法

要使程序在多核CPU上高效运行,需要遵循以下几点:
- 使用大量进程 :保持所有CPU始终处于忙碌状态,进程数量应与CPU数量相匹配。最好让各个进程完成相似的工作量,避免一个进程承担大量工作而其他进程工作量极少的情况。在许多应用中,如果应用本身具有内在的并行性,就无需额外进行并行化处理。
- 避免副作用 :副作用会阻碍并发。在具有共享内存和线程的语言中,多个线程同时写入公共内存可能会导致灾难。而Erlang没有共享内存,但存在共享ETS或DETS表的情况。使用共享ETS或DETS表时需要注意:
- 确保同一时间只有一个进程写入表,其他进程只读。
- 写入表的进程必须正确,不能写入错误数据。
- ETS表的 protected 类型更安全,只有所有者进程可以写入,但所有进程都能读取。使用 private 类型的ETS表可以确保程序安全。
- 避免顺序瓶颈 :某些操作本质上是顺序的,如IO操作。每次创建注册进程时,都可能会引入顺序瓶颈。因此,应尽量避免使用注册进程,如果必须使用,要确保其能快速响应所有请求。解决顺序瓶颈通常需要改变算法,从非分布式算法转变为分布式算法。
- 编写“小消息,大计算”的代码 :减少消息传递的开销,将更多的计算放在进程内部完成。

6. 顺序代码并行化

在Erlang中,可以通过将 lists:map 替换为 pmap 来并行化顺序代码。 pmap 的实现如下:

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([], _) ->
    [].

pmap 会为列表 L 中的每个元素创建一个并行进程来计算函数 F 的结果。使用 pmap 时需要注意以下几点:
- 并发粒度 :如果函数内部的工作量较小,使用 pmap 可能会因为创建进程和等待响应的开销大于并行处理的收益而得不偿失。
- 避免创建过多进程 pmap(F, L) 会为列表 L 中的每个元素创建一个进程,如果列表非常大,会创建大量进程。应创建适量的进程,即“lagom”数量的进程。
- 考虑所需的抽象 pmap 可能不是最合适的抽象方式。可以根据具体需求编写不同的并行映射函数,例如不关心返回值顺序的 pmap1

pmap1(F, L) ->
    S = self(),
    Ref = erlang:make_ref(),
    foreach(fun(I) ->
        spawn(fun() -> do_f1(S, Ref, F, I) end)
    end, L),
    %% gather the results
    gather1(length(L), Ref, []).

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

gather1(0, _, L) -> L;
gather1(N, Ref, L) ->
    receive
        {Ref, Ret} -> gather1(N-1, Ref, [Ret|L])
    end.

通过合理组织文件系统、遵循多核编程原则以及并行化顺序代码,我们可以充分发挥Erlang的优势,提高程序在多核CPU上的性能和可维护性。在实际开发中,需要根据具体情况灵活运用这些方法,不断优化程序。

Erlang编程:文件系统组织、多核编程与并行化策略

7. 分布式算法解决顺序瓶颈

当程序中出现顺序瓶颈时,改变算法是常见的解决方法。以分布式票务预订系统为例,传统的单机构预订方式会引入顺序瓶颈。可以采用分布式的方式,将票务分配给多个代理机构:
- 初始时,将偶数编号的票分配给第一个代理机构,奇数编号的票分配给第二个代理机构,这样可以保证不会重复售票。
- 当某个代理机构的票售罄时,可以向其他代理机构请求一批票。

这种方法虽然简单,但可以有效缓解顺序瓶颈。不过,在实际应用中,需要考虑更多复杂的情况,如代理机构的加入、离开和崩溃等。这涉及到分布式哈希表等研究领域,相关研究文献丰富,但在传统编程语言库中的应用相对较少。

8. 并行化顺序代码的更多考虑

在使用 pmap 并行化顺序代码时,除了前面提到的注意事项,还需要考虑代码的副作用。 map pmap 存在语义上的细微差异:
- 当 F(H) 有修改进程字典的代码时,调用 map 时,对进程字典的修改会在调用 map 的进程中进行;而调用 pmap 时,每个 F(H) 都在自己的进程中执行,对进程字典的修改不会影响调用 pmap 的程序的进程字典。

因此,有副作用的代码不能简单地通过将 map 替换为 pmap 来并行化。

9. 多核编程的设计问题与 mapreduce 实现

在处理更复杂的问题时,需要考虑更多的设计问题。 mapreduce 是一种由谷歌开发的用于在处理元素集合上执行并行计算的抽象方法。下面我们将实现一个 mapreduce 函数,并展示如何用它来编程一个全文索引引擎。

mapreduce 的基本思想是将一个大问题分解为多个小问题( map 阶段),然后将这些小问题的结果合并( reduce 阶段)。以下是一个简单的 mapreduce 实现示例:

mapreduce(MapFun, ReduceFun, Input) ->
    %% Map阶段
    Mapped = [MapFun(X) || X <- Input],
    %% 合并中间结果
    Intermediate = lists:flatten(Mapped),
    %% 分组中间结果
    Grouped = group_by_key(Intermediate),
    %% Reduce阶段
    [ReduceFun(Key, Values) || {Key, Values} <- Grouped].

group_by_key(List) ->
    group_by_key(List, #{}).

group_by_key([{Key, Value}|Rest], Acc) ->
    NewAcc = case maps:find(Key, Acc) of
        {ok, Values} -> maps:put(Key, [Value|Values], Acc);
        error -> maps:put(Key, [Value], Acc)
    end,
    group_by_key(Rest, NewAcc);
group_by_key([], Acc) ->
    maps:to_list(Acc).

这个 mapreduce 函数接受三个参数: MapFun 用于将输入元素映射为键值对, ReduceFun 用于合并具有相同键的值, Input 是输入数据。

使用 mapreduce 实现全文索引引擎的流程如下:

graph TD
    A[输入文档集合] --> B[Map阶段:提取单词和文档ID]
    B --> C[合并中间结果]
    C --> D[分组中间结果]
    D --> E[Reduce阶段:统计单词在文档中的出现次数]
    E --> F[生成全文索引]

具体步骤如下:
1. Map阶段 :遍历文档集合,提取每个单词和对应的文档ID,生成键值对。
2. 合并中间结果 :将所有键值对合并为一个列表。
3. 分组中间结果 :根据键(单词)对值(文档ID)进行分组。
4. Reduce阶段 :统计每个单词在不同文档中的出现次数。
5. 生成全文索引 :根据统计结果生成全文索引。

10. 总结

在多核CPU时代,充分利用多核性能是提高程序效率的关键。通过合理组织文件系统、遵循多核编程原则和并行化顺序代码,可以让Erlang程序在多核CPU上高效运行。具体来说:
- 合理组织文件系统,将应用不同部分的文件放在明确的位置,有助于提高代码的可维护性和可读性。
- 采用消息传递并发模型,避免共享状态并发带来的问题,实现程序的并行化。
- 让程序在多核CPU上高效运行,需要使用大量进程、避免副作用、避免顺序瓶颈和编写“小消息,大计算”的代码。
- 并行化顺序代码时,要注意并发粒度、进程数量和代码的副作用。
- 对于复杂问题,可以使用 mapreduce 等抽象方法进行并行计算。

在实际开发中,需要根据具体情况灵活运用这些方法,不断优化程序,以充分发挥多核CPU的性能优势。

以下是一个总结表格,概括了让程序在多核CPU上高效运行的要点:
| 要点 | 说明 |
| — | — |
| 使用大量进程 | 进程数量与CPU数量匹配,各进程工作量相似 |
| 避免副作用 | 注意共享ETS或DETS表的使用 |
| 避免顺序瓶颈 | 改变算法,采用分布式算法 |
| 编写“小消息,大计算”代码 | 减少消息传递开销 |
| 并行化顺序代码 | 注意并发粒度、进程数量和副作用 |
| 复杂问题处理 | 使用 mapreduce 等抽象方法 |

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值