前面已经讲完了erlang的函数式语法部分,接下来就要看关键的erlang并发部分了。
首先来看看erlang并发的三个要素:1. 创建进程 2.发送消息 3. 接收消息
创建进程
Erlang使用spawn/1来创建进程,如下所示:
1> F = fun() -> 2 + 2 end.
#Fun<erl_eval.20.67289768>
2> spawn(F)
.
<0.44.0>
spawn/1函数返回的是一个进程标识符pid,为了看清楚F的执行结果,可以使用如下方式:
3> spawn(fun() -> io:format("~p~n",[2 + 2]) end).
4
<0.46.0>
接下来我将会启动是个进程并且通过timer:sleep/1来暂停它们。
4> G = fun(X) -> timer:sleep(10), io:format("~p ", [X]) end.
#Fun<erl_eval.6.13229925>
5> [spawn(fun() -> G(X) end) || X <- lists:seq(1,10)].
[<0.273.0>,<0.274.0>,<0.275.0>,<0.276.0>,<0.277.0>,
<0.278.0>,<0.279.0>,<0.280.0>,<0.281.0>,<0.282.0>]
2 1 4 3 5 8 7 6 10 9
你会发现,输出的顺序并不确定,而且每次输出结果不一定,这就是erlang中并行的特征。
发送消息
erlang中使用!来发送消息,左边是pid,右边可以接受任何类型的erlang变量。如下所示
9> self() ! hello.
hello
接收消息
-module(dolphins).
-compile(export_all).
dolphin1() ->
receive
do_a_flip ->
io:format("How about no?~n");
fish ->
io:format("So long and thanks for all the fish!~n");
_ ->
io:format("Heh, we're smarter than you humans.~n")
end.
可以看到receive在语法上和case…of类似,并且case…of一样,可以在Pattern中使用Guards语句。接下来,我们使用spawn/3函数来进行新进程的创建,它的三个入参分别是模块名、函数名和函数的参数。
11> c(dolphins).
{ok,dolphins}
12> Dolphin = spawn(dolphins, dolphin1, []).
<0.40.0>
13> Dolphin ! "oh, hello dolphin!".
Heh, we're smarter than you humans.
"oh, hello dolphin!"
14> Dolphin ! fish
fish
接下来,我们如何想消息发送者回送消息呢?我们可以将发送者的pid一起作为消息发送给接收者,接收者再通过此pid回送消息即可
dolphin2() ->
receive
{From, do_a_flip} ->
From ! "How about no?";
{From, fish} ->
From ! "So long and thanks for all the fish!";
_ ->
io:format("Heh, we're smarter than you humans.~n")
end.
如下,我们即可看到消息回送的过程
11> c(dolphins).
{ok,dolphins}
12> Dolphin2 = spawn(dolphins, dolphin2, []).
<0.65.0>
13> Dolphin2 ! {self(), do_a_flip}.
{<0.32.0>,do_a_flip}
14> flush().
Shell got "How about no?"
ok
上面的程序你可以发现,dolphin接收完一次之后就结束了进程,为了让进程一直监听接收消息,可以使用递归来解决这个问
dolphin3() ->
receive
{From, do_a_flip} ->
From ! "How about no?",
dolphin3();
{From, fish} ->
From ! "So long and thanks for all the fish!";
_ ->
io:format("Heh, we're smarter than you humans.~n"),
dolphin3()
end.
当发送fish指令时,就会停止进程。
接下来看看下面的冰箱模块,这个冰箱进程只允许两个操作:存食物和取食物。
fridge1() ->
receive
{From, {store, _Food}} ->
From ! {self(), ok},
fridge1();
{From, {take, _Food}} ->
From ! {self(), not_found},
fridge1();
terminate ->
ok
end.
虽然上面有了存取两个操作,但是并未实际状态数据的存储,为此,我们仍然采用递归的方式,如下所示:
fridge2(FoodList) ->
receive
{From, {store, Food}} ->
From ! {self(), ok},
fridge2([Food|FoodList]);
{From, {take, Food}} ->
case lists:member(Food, FoodList) of
true ->
From ! {self(), {ok, Food}},
fridge2(lists:delete(Food, FoodList));
false ->
From ! {self(), not_found},
fridge2(FoodList)
end;
terminate ->
ok
end.
具体的演示效果大家可以自行试一试,但是相信这段程序会让大部分的程序员比较苦恼,因为如果你要使用冰箱,就必须要先了解和它通信的机制。而如何避免这个不必要的负担呢,可以使用函数来接收和发送参数:
store(Pid, Food) ->
Pid ! {self, {store, Food}},
receive
{Pid, Msg} -> Msg
end.
take(Pid, Food) ->
Pid ! {self, {take, Food}},
receive
{Pid, Msg} -> Msg
end.
如上即完成了冰箱存取方法的定义,这样我们就不用再关心message时如何工作的,甚至,我们还可以将进程的创建也进行隐藏
start(FoodList) ->
spawn(?MODULE, fridge2, [FoodList]).
(注:?MODULE返回当前的模块名)
超时问题
接下来做一个试验,传一个没有的pid给take方法,这时会发现shell冻结住了
20> kitchen:take(pid(0,250,0), dog).
原因很好理解,receive并没有接收到任何信息,一直阻塞,因此需要超时机制,如下所示,3秒之后take和store的receive方法
store(Pid, Food) ->
Pid ! {self(), {store, Food}},
receive
{Pid, Msg} -> Msg
after 3000 ->
timeout
end.
take(Pid, Food) ->
Pid ! {self(), {take, Food}},
receive
{Pid, Msg} -> Msg
after 3000 ->
timeout
end.
需要注意的就是,receive中的模式匹配会从最旧的消息开始遍历消息队列,遇到匹配的时将此消息取出,带来的副作用就是如果消息队列中遗留了太多的污染消息(不匹配),就会导致进程消息处理的效率问题。