基于Erlang实现的一个简单的并发控制程序

这篇文章是我之前在RYTong内部分享的一篇文章,摘取了有用的部分。当时帮助另一个某项目解决一个并发控制的问题,基于此跟大家分享一个关于Erlang process的小程序。

问题背景

在开始解决之前先了解下具体问题吧。当时该项目上有一个批处理的功能,会在数据库中读取一个数据列表出来,针对每一个列表元素启动一个Erlang process,使用数据向银行后台发送请求。很不幸的,银行后台承受不了这样大的压力,希望我们在批处理时进行并发控制。

了解具体问题之后,我的思路如下:

  1. 我们需要提供一个API,输入一个任务列表、一个批处理函数和一个最大并发数,实现并发的调用批处理函数处理任务列表的每一个元素,并且同时间存在的Erlang process数量不能超过最大并发数。
  2. 为了对批处理的process(worker process)数量进行管理,我们需要一个监工monitor。
  3. 这个monitor需要准确的知道当前worker process的数量,因此worker process的创建需要由monitor完成,并且worker process完成任务后需要通知monitor。
  4. 在worker process数量达到上限时,程序需要等待,直到monitor为当前的任务创建了新的worker process。

看看代码


-module(concurrency_control).

-export ([batch_work/3]).

batch_work(WorkList, BatchFunc, WorkersNum) ->
    {ok, Pid} = start_monitor(WorkersNum, BatchFunc),
    io:format("monitor started ~p~n", [Pid]),
    do_batch_work(WorkList, Pid, WorkersNum, BatchFunc).

do_batch_work([], Pid, _, _) ->
    Pid ! {self(), stop},
    ok;
do_batch_work([H|T] = List, Pid, WorkersNum, BatchFunc) ->
    case is_process_alive(Pid) of
        true ->
            Pid ! {self(), exec, H},
            receive
                {Pid, ok} -> 
                    do_batch_work(T, Pid, WorkersNum, BatchFunc)
            end;
        false ->
            batch_work(List, BatchFunc, WorkersNum)
    end.

start_monitor(Num, BatchFunc) ->
    Parent = self(),
    Pid = spawn(fun() -> concurrency_monitor(Num, BatchFunc, Parent) end),
    receive
        {Pid, started} ->
            {ok, Pid}
    end.

concurrency_monitor(Num, BatchFunc, Parent) ->
    process_flag(trap_exit, true),
    Parent ! {self(), started},
    loop(0, Num, BatchFunc, Parent).

loop(0, Max, BatchFunc, Parent) ->
    receive
        {From, exec, Arg} ->
            spawn_link(fun() -> catch BatchFunc(Arg) end),
            From ! {self(), ok},
            loop(1, Max, BatchFunc, Parent);
        {Parent, stop} ->
            stop
    end;  
loop(Max, Max, BatchFunc, Parent) ->
    receive
        {'EXIT', _Worker, _Reason} ->
            loop(Max - 1, Max, BatchFunc, Parent)
    end;    
loop(Current, Max, BatchFunc, Parent) ->
    receive
        {From, exec, Arg} ->
            spawn_link(fun() -> catch BatchFunc(Arg) end),
            From ! {self(), ok},
            loop(Current + 1, Max, BatchFunc, Parent);
        {'EXIT', _Worker, _Reason} ->
            loop(Current - 1, Max, BatchFunc, Parent)
    end.

解释如下:

  • 在API实现的第一句代码,我便创建了一个monitor process,monitor执行的函数为concurrency_monitor/3。
  • 在concurrency_monitor/3中调用了process_flag(trap_exit, true),这个方法会捕获worker process退出的消息(无论正常或异常)。前提条件是worker process与monitor建立了link,因此我在loop函数中使用了spawn_link函数创建worker process。
  • monitor的循环体就是loop函数,循环期间monitor会接收三种消息。{From, exec, Arg}消息会创建worker process,并以Arg为参数调用批处理函数,创建完成后向From发送{self(), ok}消息,通知From创建成功。{‘EXIT’, _Worker, _Reason}消息为worker process退出的消息,此时monitor会将当前worker数量减一。{Parent, stop}消息会让monitor退出。
  • 创建完monitor后,do_batch_work函数会遍历任务列表,使用每一个元素向monitor发送{self(), exec, H}消息。并接受monitor发送的{Pid, ok}消息。
  • 通过47到51行的代码,我们可以知道当worker数量达到最大值时,monitor不会处理{From, exec, Arg}消息,因此此时do_batch_work会在阻塞在18行的receive处。直到一个worker完成工作后,向monitor发送{‘EXIT’, _Worker, _Reason}消息,monitor才会继续处理{From, exec, Arg}消息来创建worker进程。
  • 当遍历完任务列表后,程序会向monitor发送{self(), stop}消息,通知monitor退出。大家可能会觉得这样做有问题,因为monitor可能还有没创建的worker,提前退出会使得任务列表没有被全部处理。其实是没关系的,因为Erlang process是顺序处理消息的,因为程序是最后发送{self(), stop}消息的,所以monitor一定会创建完所有worker之后退出的。
  • 仔细分析38到46行的代码,monitor不止会在创建完所有worker之后退出,还会等待所有worker完成任务后再处理{self(), stop}消息。

示例

8> L= lists:seq(1, 20).
[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]
9> Func = fun(Input) -> io:format("~p: execute task with ~p~n", [time(), Input]), timer:sleep(1000) end.
10> concurrency_control:batch_work(L, Func, 4).
monitor started <0.134.0>
{10,40,34}: execute task with 1
{10,40,34}: execute task with 2
{10,40,34}: execute task with 3
{10,40,34}: execute task with 4
{10,40,35}: execute task with 5
{10,40,35}: execute task with 6
{10,40,35}: execute task with 7
{10,40,35}: execute task with 8
{10,40,36}: execute task with 9
{10,40,36}: execute task with 10
{10,40,36}: execute task with 11
{10,40,36}: execute task with 12
{10,40,37}: execute task with 13
{10,40,37}: execute task with 14
{10,40,37}: execute task with 15
{10,40,37}: execute task with 16
{10,40,38}: execute task with 17
{10,40,38}: execute task with 18
{10,40,38}: execute task with 19
{10,40,38}: execute task with 20
ok

上述示例代码中,批处理函数会打印当前执行的时间并sleep 1秒,同时设置了最大并发数为4。这样我们便会看到每秒只会并发执行4个任务,打印四句日志。

结语

Erlang在编写并发程序时,为我们提供了gen_server、gen_fsm等等设计模式,可以很好的封装并发编程的细节。使用这些模式,也会让代码变得简洁易懂。这个例子中,并没有使用相关模式,为了是让大家熟悉Erlang并发相关的基础,巩固基础之后再使用这些设计模式会起到锦上添花的作用,否则适得其反。

希望这个示例能帮到大家。荆轲刺秦王

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值