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

来源:互联网 发布:1024邀请码多少钱淘宝 编辑:程序博客网 时间:2024/05/16 00:31

这篇文章是我之前在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 20ok

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

结语

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

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

0 0
原创粉丝点击