并发进程执行顺序
Concurrent process execution order
试图弄清楚 Erlang 并发是如何工作的。为了进行测试,我有以下模块:
server.erl:
-module(server).
-export([loop/0]).
loop() ->
receive
{foo, Msg_foo} ->
io:format("~w~n", [Msg_foo]),
loop();
{bar, Msg_bar} ->
io:format("~w~n", [Msg_bar]),
loop();
stop ->
io:format("~s~n", ["End server process"]),
true
end.
process_a.erl
-module(process_a).
-export([go_a/0]).
go_a() ->
receive
{foo, Pid1} ->
Pid1 ! {foo, 'Message foo from process A'},
go_a();
{bar, Pid2} ->
Pid2 ! {bar, 'Message bar from process A'},
go_a()
end.
process_b.erl
-module(process_b).
-export([go_b/0]).
go_b() ->
receive
{foo, Pid1} ->
Pid1 ! {foo, 'Message foo from process B'},
go_b();
{bar, Pid2} ->
Pid2 ! {bar, 'Message bar from process B'},
go_b()
end.
client.erl
-module(client).
-export([start/0]).
-import(server, [loop/0]).
-import(process_a, [go_a/0]).
-import(process_b, [go_b/0]).
go() ->
Server_Pid = spawn(server, loop, []),
Pid_A = spawn(process_a, go_a, []),
Pid_B = spawn(process_b, go_b, []),
Pid_A ! {foo, Server_Pid},
Pid_B ! {bar, Server_Pid},
Pid_A ! {bar, Server_Pid},
Pid_B ! {foo, Server_Pid},
Pid_A ! {foo, Server_Pid},
Pid_B ! {foo, Server_Pid},
Pid_A ! {bar, Server_Pid},
Pid_B ! {bar, Server_Pid}.
start() ->
go().
客户端向进程A和进程B发送消息,进程B又向服务器发送消息。消息的顺序是:
A foo
B bar
A bar
B foo
A foo
B foo
A bar
B bar
但是程序输出是:
'Message foo from process A'
'Message bar from process A'
'Message foo from process A'
'Message bar from process A'
'Message bar from process B'
'Message foo from process B'
'Message foo from process B'
'Message bar from process B'
服务器先处理进程A的所有消息,再处理进程B的所有消息。我的问题是,消息处理顺序是由什么决定的?我以为是消息接收的顺序。
全靠进程调度。在您的客户端代码启动服务器和进程 A 和 B 之后,这些进程是新创建的,但甚至可能还没有任何时间执行(如果有,它们将立即在接收时挂起)。客户端代码继续执行并快速向 A 和 B 发送一堆消息。这些是异步操作,客户端进程在从调用 go() 返回之前根本不需要挂起。
一旦挂起的进程收到消息,它就会准备好被安排执行,但在这发生之前可能需要一小部分时间。与此同时,更多的消息可能会不断到达他们的邮箱,所以当 A 或 B 实际开始时 运行,他们的邮箱中可能已经有来自客户端的所有四条消息。一般来说,您也不能确定 A 和 B 中的哪一个会先开始执行,即使在像这样的简单情况下调度可能是非常可预测的。
所以在你的例子中,A 在 B 之前被调度,它开始执行,并且在很短的时间内它消耗了它的所有消息。这不需要太多工作,所以 A 甚至不会花费整个时间片。然后由于其邮箱为空而暂停。然后 B 得到安排并做同样的事情。
如果有很多进程,and/or 很多工作,Erlang VM 可以将进程拆分到不同 OS 线程上的调度程序(运行 真正并行时尚,如果你有一个多核 CPU)。但由于示例非常简单,这些进程可能在单个调度程序中处理,因此排序变得更加可预测。如果 A 和 B 的队列中都有数千条消息,或者每条消息都需要大量的计算工作来处理,您会看到消息交错。
(顺便说一句,您在客户端中的导入声明什么都不做,因为您使用的是 spawn(Module, Fname, Args)。如果您编写了例如 spawn(fun() -> loop() end) 它们将需要。)
试图弄清楚 Erlang 并发是如何工作的。为了进行测试,我有以下模块:
server.erl:
-module(server).
-export([loop/0]).
loop() ->
receive
{foo, Msg_foo} ->
io:format("~w~n", [Msg_foo]),
loop();
{bar, Msg_bar} ->
io:format("~w~n", [Msg_bar]),
loop();
stop ->
io:format("~s~n", ["End server process"]),
true
end.
process_a.erl
-module(process_a).
-export([go_a/0]).
go_a() ->
receive
{foo, Pid1} ->
Pid1 ! {foo, 'Message foo from process A'},
go_a();
{bar, Pid2} ->
Pid2 ! {bar, 'Message bar from process A'},
go_a()
end.
process_b.erl
-module(process_b).
-export([go_b/0]).
go_b() ->
receive
{foo, Pid1} ->
Pid1 ! {foo, 'Message foo from process B'},
go_b();
{bar, Pid2} ->
Pid2 ! {bar, 'Message bar from process B'},
go_b()
end.
client.erl
-module(client).
-export([start/0]).
-import(server, [loop/0]).
-import(process_a, [go_a/0]).
-import(process_b, [go_b/0]).
go() ->
Server_Pid = spawn(server, loop, []),
Pid_A = spawn(process_a, go_a, []),
Pid_B = spawn(process_b, go_b, []),
Pid_A ! {foo, Server_Pid},
Pid_B ! {bar, Server_Pid},
Pid_A ! {bar, Server_Pid},
Pid_B ! {foo, Server_Pid},
Pid_A ! {foo, Server_Pid},
Pid_B ! {foo, Server_Pid},
Pid_A ! {bar, Server_Pid},
Pid_B ! {bar, Server_Pid}.
start() ->
go().
客户端向进程A和进程B发送消息,进程B又向服务器发送消息。消息的顺序是:
A foo
B bar
A bar
B foo
A foo
B foo
A bar
B bar
但是程序输出是:
'Message foo from process A'
'Message bar from process A'
'Message foo from process A'
'Message bar from process A'
'Message bar from process B'
'Message foo from process B'
'Message foo from process B'
'Message bar from process B'
服务器先处理进程A的所有消息,再处理进程B的所有消息。我的问题是,消息处理顺序是由什么决定的?我以为是消息接收的顺序。
全靠进程调度。在您的客户端代码启动服务器和进程 A 和 B 之后,这些进程是新创建的,但甚至可能还没有任何时间执行(如果有,它们将立即在接收时挂起)。客户端代码继续执行并快速向 A 和 B 发送一堆消息。这些是异步操作,客户端进程在从调用 go() 返回之前根本不需要挂起。
一旦挂起的进程收到消息,它就会准备好被安排执行,但在这发生之前可能需要一小部分时间。与此同时,更多的消息可能会不断到达他们的邮箱,所以当 A 或 B 实际开始时 运行,他们的邮箱中可能已经有来自客户端的所有四条消息。一般来说,您也不能确定 A 和 B 中的哪一个会先开始执行,即使在像这样的简单情况下调度可能是非常可预测的。
所以在你的例子中,A 在 B 之前被调度,它开始执行,并且在很短的时间内它消耗了它的所有消息。这不需要太多工作,所以 A 甚至不会花费整个时间片。然后由于其邮箱为空而暂停。然后 B 得到安排并做同样的事情。
如果有很多进程,and/or 很多工作,Erlang VM 可以将进程拆分到不同 OS 线程上的调度程序(运行 真正并行时尚,如果你有一个多核 CPU)。但由于示例非常简单,这些进程可能在单个调度程序中处理,因此排序变得更加可预测。如果 A 和 B 的队列中都有数千条消息,或者每条消息都需要大量的计算工作来处理,您会看到消息交错。
(顺便说一句,您在客户端中的导入声明什么都不做,因为您使用的是 spawn(Module, Fname, Args)。如果您编写了例如 spawn(fun() -> loop() end) 它们将需要。)