《Learn You Some Erlang for Great Good!》的学习笔记(九)

     前面已经讲完了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 ! fishfish 

接下来,我们如何想消息发送者回送消息呢?我们可以将发送者的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中的模式匹配会从最旧的消息开始遍历消息队列,遇到匹配的时将此消息取出,带来的副作用就是如果消息队列中遗留了太多的污染消息(不匹配),就会导致进程消息处理的效率问题。

发布了42 篇原创文章 · 获赞 9 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/jjxojm/article/details/52649760