"One of the main reasons for using Erlang instead of other functional languages is Erlang's ability to handle concurrency and distributed programming." —— 《Erlang User's Guide》
今天就玩一下Erlang的并发(concurrency)。
当年我还是写C++的客户端开发,后来因为公司需要被调到服务端开始了Erlang之路。当时函数式编程对于我来说还是非常新鲜的,那奇特的语法让已经习惯OO语法的我感觉到非常别扭,还好部门老大会对新手进行了一些基础的培训,每次培训之后都会留下几个小题目,其中有一题是通过spawn来展示Erlang的并发。我记得我当时是没完成这道题目的,部门老大可能太忙了,也忘记检查,那今天就把当初的作业好好的补一下。
Erlang的并发,其实就是开多个进程同时干活,这里的进程是Erlang VM里的概念,与操作系统的进程不是同一概念。Erlang的进程是异步(asynchronous)执行的,举个简单例子,spawn 10个进程来分别输出1到10,其执行结果是乱序的,而不是1到10的顺序排列!
先来试试顺序执行:
1> [io:format("~p ~n", [Index]) || Index <- lists:seq(1, 10)]. 1 2 3 4 5 6 7 8 9 10 [ok,ok,ok,ok,ok,ok,ok,ok,ok,ok]
结果很明显,就是1到10的顺序输出。
接下来开10个进程,也是输出1到10,看看结果如何:
2> [spawn(fun() -> io:format("~p ~n", [Index]) end) || Index <- lists:seq(1, 10)]. 1 2 3 4 5 6 7 8 9 10
咦,看上去结果和顺序执行一模一样,但再多试几次:
8> [spawn(fun() -> io:format("~p ~n", [Index]) end) || Index <- lists:seq(1, 10)]. 1 2 3 4 5 7 6 8 9 10 10> [spawn(fun() -> io:format("~p ~n", [Index]) end) || Index <- lists:seq(1, 10)]. 1 2 3 4 6 7 5 8 9 10
这次结果终于体现出异步了,因为这段代码执行起来几乎不用进行计算,几乎是进程一启动就完成了,所以很大几率会出现按1到10顺序输出的结果,所以要多试几次。老大布置的题目答案其实就是这么一行代码,想当年我是真▪菜鸟啊,哈哈。
这就是Erlang的多进程并发异步执行,然而,这看上去并没有什么x用,因为上面的例子太简单了,多进程并发的真正威力并没有体现出来,是时候表演真正的技术了。
接下来,分别用Erlang的单进程和多进程分别计算1到10万的乘积,并比较两者间的运行时间,先上代码:
1 -module(concurrency_test). 2 -compile(export_all). 3 4 %% 单线程计算 5 %% 计算1到Num的乘积 6 single_process(Num) -> 7 single_process(1, Num). 8 9 single_process(Result, 0) -> 10 Result; 11 single_process(Result, Index) -> 12 single_process(Result * Index, Index - 1). 13 14 %% 多线程计算 15 %% 计算1到Num的乘积 16 %% Count为开启进程的个数 17 multiple_processes(Num, Count) -> 18 Pid = self(), 19 lists:foreach(fun(Index) -> 20 spawn(fun() -> 21 BeginNum = trunc(Num / Count * (Index - 1)) + 1, 22 EndNum = trunc(Num / Count * Index), 23 multiple_processes(Pid, BeginNum, EndNum) 24 end) 25 end, lists:seq(1, Count)), 26 loop(1, Count). 27 28 loop(Total, Count) -> 29 receive 30 {result, Result} -> 31 case Count - 1 of 32 0 -> 33 Total * Result; 34 _ -> 35 loop(Total * Result, Count - 1) 36 end; 37 _ -> 38 loop(Total, Count) 39 end. 40 41 multiple_processes(Pid, BeginNum, EndNum) -> 42 Pid ! {result, calculate(BeginNum, EndNum)}. 43 44 calculate(BeginNum, EndNum) -> 45 calculate(1, BeginNum, EndNum). 46 47 calculate(Result, BeginNum, BeginNum) -> 48 Result * BeginNum; 49 calculate(Result, BeginNum, EndNum) -> 50 calculate(Result * BeginNum, BeginNum + 1, EndNum). 51 52 tc(F) -> 53 {T, _} = timer:tc(F), 54 io:format("消耗时间:~p 秒 ~n", [T / 1000000]).
这段代码有点小复杂,但应该不影响理解,先来试试运行结果是否正确,试试1到4的计算。
2> c(concurrency_test). {ok,concurrency_test} 3> concurrency_test:single_process(4). 24 4> concurrency_test:multiple_processes(4, 2). 24
结果无误,再来试试1到100。
5> concurrency_test:single_process(100). 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000 6> concurrency_test:multiple_processes(100, 2). 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000
也无问题,看来结果是正确的,现在忽略结果,计算1到100000,看看单进程和2个进程的运行时间对比。
17> concurrency_test:tc(fun() -> concurrency_test:single_process(100000) end). 消耗时间:7.62 秒 ok 18> concurrency_test:tc(fun() -> concurrency_test:multiple_processes(100000, 2) end). 消耗时间:2.94 秒 ok
WOW,2个人干活果然比一个人干活快,如果4个人呢?
19> concurrency_test:tc(fun() -> concurrency_test:multiple_processes(100000, 4) end). 消耗时间:1.614 秒 ok
4个人干活更快了,那8个人呢?
20> concurrency_test:tc(fun() -> concurrency_test:multiple_processes(100000, 8) end). 消耗时间:1.375 秒 ok
还更快!如果16个人会更快吗?
21> concurrency_test:tc(fun() -> concurrency_test:multiple_processes(100000, 16) end). 消耗时间:1.389 秒 ok
咦,好像还慢了。测试的机器CPU是E3-1231 v3,四核心八线程,个人推测满载的时候最多也就调用8个逻辑核心进行计算,这里没有深究。
下面计算1到100万,看看单进程和8进程时CPU的负载。
单进程时:
基本上一直是在占用16%左右的CPU资源。
8个进程时:
基本上是把CPU给压榨光了。
现在终于能够感受到Erlang多进程并发计算时的威力了吧,在计算1到10万的乘积时,因为中间的计算过程可以是乱序的,不影响最终的运算,所以可以用多进程来异步计算,其效率的提升也是非常的明显。
其实这个示例还是存在缺陷,在计算1到1000000时,第一个开启的进程计算1到125000,第八个开启的进程计算875001到1000000,这两个进程的执行的计算量不在同一个数量级上面,第八个进程就会成为短板,因此这个示例可以优化,使每个进程执行相近的计算量,计算效率会更高。