1
1
11-进程
2
2
=======
3
3
4
- Elixir中,所有代码都在进程中执行 。进程彼此独立,一个接一个并发执行,彼此通过消息传递来沟通 。
5
- 进程不仅仅是Elixir中并发的基础 ,也是Elixir创建分布式、高容错程序的本质。
4
+ Elixir里所有代码都在进程中执行 。进程彼此独立,并发执行,通过传递消息(message)进行沟通 。
5
+ 进程不仅仅是Elixir并发编程的基础 ,也是Elixir创建分布式、高容错程序的本质。
6
6
7
- Elixir的进程和操作系统中的进程不可混为一谈 。
8
- Elixir的进程,在CPU和内存使用上, 是极度轻量级的(不同于其它语言中的线程 )。
9
- 因此 ,同时运行着数十万、百万个进程也并不是罕见的事。
7
+ Elixir的进程和传统操作系统中的进程不可混为一谈 。
8
+ Elixir的进程在CPU和内存使用上, 是极度轻量级的(但不同于其它语言中的线程 )。
9
+ 正因如此 ,同时运行着数十万、百万个进程也并不是罕见的事。
10
10
11
- 本章将讲解如何派生新进程,以及在进程间如何发送和接受消息等基本知识 。
11
+ 本章将讲解如何派生新进程,以及在不同进程间发送和接受消息等基本知识 。
12
12
13
- ##11.1-进程派生
13
+ ##进程派生(spawning)
14
+
15
+ 派生(spawning)一个新进程的方法是使用自动导入(kernel模块)的``` spawn/1 ``` 函数:
14
16
15
- 派生(spawning)一个新进程的方法是使用自动导入(kernel函数)的``` spawn/1 ``` 函数:
16
17
``` elixir
17
18
iex> spawnfn -> 1 + 2 end
18
19
# PID<0.43.0>
19
20
```
20
21
21
22
函数``` spawn/1 ``` 接收一个_函数_作为参数,在其派生出的进程中执行这个函数。
22
23
23
- 注意spawn/1返回一个PID(进程标识)。在这个时候,这个派生的进程很可能已经结束。
24
+ 注意,``` spawn/1 ``` 返回一个PID(进程标识)。在这个时候,这个派生的进程很可能已经结束。
25
+
24
26
派生的进程执行完函数后便会结束:
27
+
25
28
``` elixir
26
29
iex> pid= spawnfn -> 1 + 2 end
27
30
# PID<0.44.0>
44
47
45
48
可以发送和接收消息,让进程变得越来越有趣。
46
49
47
- ##11.2-发送和接收
50
+ ##发送和接收(消息)
51
+
48
52
使用``` send/2 ``` 函数发送消息,用``` receive/1 ``` 接收消息:
53
+
49
54
``` elixir
50
55
iex> sendself (), {:hello ," world" }
51
56
{:hello ," world" }
@@ -56,12 +61,13 @@ iex> receive do
56
61
" world"
57
62
```
58
63
59
- 当有消息被发给某进程,该消息就被存储在该进程的邮箱里 。
64
+ 当有消息被发给某进程,该消息就被存储在该进程的“邮箱”里 。
60
65
语句块``` receive/1 ``` 检查当前进程的邮箱,寻找匹配给定模式的消息。
61
- 其中函数``` receive/1 ``` 支持分支子句,如``` case/2 ``` 。
62
- 当然也可以给子句加上卫兵表达式。
66
+ 函数``` receive/1 ``` 支持卫兵语句(guards)及分支子句(clause)如``` case/2 ``` 。
67
+
68
+ 如果找不到匹配的消息,当前进程将一直等待,直到下一条信息到达。
63
69
64
- 如果找不到匹配的消息,当前进程将一直等待,知道下一条信息到达。但是可以设置一个超时时间 :
70
+ 可以给等待设置一个超时时间 :
65
71
``` elixir
66
72
iex> receive do
67
73
.. .> {:hello , msg}- > msg
@@ -85,7 +91,7 @@ iex> receive do
85
91
" Got hello from #PID<0.48.0>"
86
92
```
87
93
88
- 在shell中执行程序时,辅助函数``` flush/0 ``` 很有用。它清空缓冲区,打印进程邮箱中的所有消息 :
94
+ 在shell中执行程序时,辅助函数``` flush/0 ``` 很有用。它清空并打印进程邮箱中的所有消息 :
89
95
``` elixir
90
96
iex> sendself (),:hello
91
97
:hello
@@ -94,16 +100,25 @@ iex> flush()
94
100
:ok
95
101
```
96
102
97
- ##11.3-链接
98
- Elixir中最常用的进程派生方式是通过函数``` spawn_link/1 ``` 。
99
- 在举例子讲解``` spawn_link/1 ``` 之前,来看看如果一个进程失败了会发生什么:
103
+ ##链接(links)
104
+
105
+ Elixir中最常用的进程派生方式是通过调用函数``` spawn_link/1 ``` 。
106
+
107
+ 在举例子讲解``` spawn_link/1 ``` 之前,来看看如果一个进程挂掉会发生什么:
100
108
``` elixir
101
109
iex> spawnfn -> raise " oops" end
102
110
# PID<0.58.0>
111
+
112
+ [error]Process # PID<0.58.00> raised an exception
113
+ ** (RuntimeError ) oops
114
+ :erlang .apply / 2
103
115
```
104
116
105
- 。。。啥也没发生。这时因为进程都是互不干扰的。如果我们希望一个进程中发生失败可以被另一个进程知道,我们需要链接它们。
106
- 使用``` spawn_link/1 ``` 函数,例子:
117
+ ...仅仅是打印了一些文字信息,而派生这个倒霉进程的父进程仍然全无知觉地继续运行。
118
+ 这是因为进程是互不干扰的。
119
+ 如果我们希望一个进程发生异常挂掉可以被另一个进程感知,我们需要链接它们。
120
+
121
+ 这需要使用``` spawn_link/1 ``` 函数来派生进程,例子:
107
122
``` elixir
108
123
iex> spawn_linkfn -> raise " oops" end
109
124
# PID<0.60.0>
@@ -112,8 +127,9 @@ iex> spawn_link fn -> raise "oops" end
112
127
:erlang .apply / 2
113
128
```
114
129
115
- 当失败发生在shell中,shell会自动终止执行,并显示失败信息。这导致我们没法看清背后过程。
116
- 要弄明白链接的进程在失败时发生了什么,我们在一个脚本文件使用``` spawn_link/1 ``` 并且执行和观察它:
130
+ 当这个失败发生在shell中,shell会自动捕获这个异常并显示格式优雅的异常信息。
131
+ 为了弄明白失败时真正会发生什么,让我们在一个脚本文件中使用``` spawn_link/1 ``` :
132
+
117
133
``` elixir
118
134
# spawn.exs
119
135
spawn_linkfn -> raise " oops" end
@@ -123,35 +139,70 @@ receive do
123
139
end
124
140
```
125
141
126
- 这次,该进程在失败时把它的父进程也弄停止了,因为它们是链接的。<br />
127
- 手动链接进程:``` Process.link/1 ``` 。
128
- 建议可以多看看[ Process模块] ( http://elixir-lang.org/docs/stable/elixir/Process.html ) ,里面包含很多常用的进程操作函数。
142
+ 执行:
143
+ ``` elixir
144
+ $ elixir spawn.exs
145
+
146
+ ** (EXIT from# PID<0.47.0>) an exception was raised:
147
+ ** (RuntimeError ) oops
148
+ spawn.exs: 1 : anonymousfn / 0 in :elixir_compiler_0 .__FILE__/ 1
149
+ ```
129
150
130
- 进程和链接在创建能高容错系统时扮演重要角色。在Elixir程序中,我们经常把进程链接到某“管理者”上。
131
- 由这个角色负责检测失败进程,并且创建新进程取代之。因为进程间独立,默认情况下不共享任何东西。
132
- 而且当一个进程失败了,也不会影响其它进程。
133
- 因此这种形式(进程链接到“管理者”角色)是唯一的实现方法。
151
+ 这次,该进程在失败时把它的父进程也弄停止了,因为它们是链接的。
134
152
153
+ 使用函数``` spawn_link/1 ``` 是在派生时对父子进程进行链接,
154
+ 你还可以手动对两个进程进行链接:使用函数``` Process.link/1 ``` 。
155
+ 我们推荐可以多看看[ Process模块] ( http://elixir-lang.org/docs/stable/elixir/Process.html ) ,里面包含很多常用的进程操作函数。
135
156
136
- 其它语言通常需要我们来try-catch异常,而在Elixir中我们对此无所谓,放手任进程挂掉。
157
+ 进程和链接在创建能高容错系统时扮演重要角色。
158
+ 在Elixir程序中,我们经常把进程链接到“管理者(supervisors)”上。
159
+ 由这个角色负责检测失败进程,并且创建新进程取代之。这是唯一可行的方式。
160
+ 因为进程间独立,默认情况下不共享任何东西。一个进程失败了,不会影响其它进程的状态。
161
+
162
+ 其它语言通常需要我们抛出/捕获异常,而在Elixir中我们可以放任进程挂掉,
137
163
因为我们希望“管理者”会以更合适的方式重启系统。
138
- “要死你就快一点”是Elixir软件开发的通用哲学。
164
+ “死快一点(failing fast)”是Elixir软件开发中的一个常见哲学。
165
+
166
+ ``` spawn/1 ``` 和``` spawn_link/1 ``` 是Elixir中创建进程的基本方式。
167
+ 尽管我们到目前为止都是专门调用它们,实际上大部分时间我们会使用基于它们功能的一些抽象操作。
168
+ 比如常见的:“任务(Tasks)”。
169
+
170
+ ##任务(Task)
171
+
172
+ 任务建立在进程派生函数之上,提供了更好的错误报告和内省。
173
+
174
+ ``` elixir
175
+ iex (1 )> Task .start fn -> raise " oops" end
176
+ {:ok ,# PID<0.55.0>}
177
+
178
+ 15 :22 :33 .046 [error]Task # PID<0.55.0> started from #PID<0.53.0> terminating
179
+ ** (RuntimeError ) oops
180
+ (elixir) lib/ task/ supervised.ex: 74 :Task .Supervised .do_apply / 2
181
+ (stdlib) proc_lib.erl: 239 ::proc_lib .init_p_do_apply / 3
182
+ Function :# Function<20.90072148/0 in :erl_eval.expr/5>
183
+ Args : []
184
+ ```
139
185
186
+ 我们使用``` Task.start/1 ``` 和``` Task.start_link/1 ``` 代替``` spawn/1 ``` 和``` spawn_link/1 ``` ,
187
+ 返回``` {:ok pid} ``` 而不仅仅是子进程的PID。这使得任务可以在监督者树中使用。
188
+ 另外,任务提供了许多便利的函数,比如``` Task.async/1 ``` 和``` Task.await/1 ``` 等,
189
+ 以及其它一些易于构建分布式接结构的函数。
140
190
141
- 在讲下一章之前,让我们来看一个Elixir中常见的创建进程的情形 。
191
+ 我们将在《高级》篇中介绍关于任务更多的函数功能。现在我们只需要知道,使用任务有更好的错误报告 。
142
192
143
- ##11.4-状态
144
- 目前为止我们还没有怎么谈到状态。但是,只要你创建程序,就需要状态。
145
- 例如,保存程序的配置信息,或者分析一个文件先把它保存在内存里。
146
- 你怎么存储状态?
193
+ ##状态(state)
147
194
195
+ 目前为止我们还没有怎么谈到状态。但是如果你需要构建一个程序它需要状态,比如保存程序的配置信息,
196
+ 或者解析一个文件先把它保存在内存里,你在哪儿存储?
197
+
198
+ _ 进程_就是(最常见的一个)答案。我们可以写个进程执行无限循环,保存若干状态,
199
+ 通过收发消息维护和改变这些状态值。
200
+ 举个例子,我们写一个程序模块,创建一个提供键值对存储服务的进程:
148
201
149
- 进程就是(最常见的)答案。我们可以写无限循环的进程,保存一个状态,然后通过收发信息来告知或改变该状态。
150
- 例如,写一个模块文件,用来创建一个提供k-v仓储服务的进程:
151
202
``` elixir
152
203
defmodule KV do
153
- def start do
154
- { :ok , spawn_link (fn -> loop (%{})end )}
204
+ def start_link do
205
+ Task . start_link (fn -> loop (%{})end )
155
206
end
156
207
157
208
defp loop (map)do
@@ -164,14 +215,15 @@ defmodule KV do
164
215
end
165
216
end
166
217
end
218
+
167
219
```
168
220
169
- 注意``` start ``` 函数简单地派生一个新进程,这个进程以一个空的图为参数 ,执行``` loop/1 ``` 函数。
170
- 这个 ``` loop/1 ``` 函数等待消息,并且针对每个消息执行合适的操作。
171
- 加入受到一个 ``` :get ``` 消息,它把消息发回给调用者 ,然后再次调用自身``` loop/1 ``` ,等待新消息。
172
- 当受到 ``` :put ``` 消息,它便用一个新版本的图变量(里面的k-v更新了)再次调用自身 。
221
+ 注意``` start_link ``` 函数启动一个新的进程。这个进程以一个空的图(map)为参数 ,执行``` loop/1 ``` 函数。
222
+ 启动后, ``` loop/1 ``` 函数等待消息,并且针对每个消息执行合适的操作。
223
+ 如果收到 ``` :get ``` 消息,它会把消息发回给调用者 ,然后再次调用自身``` loop/1 ``` ,等待新消息。
224
+ 如果收到 ``` :put ``` 消息,它便用一个新版本的图变量(里面保存了新的键/值)再次调用 ``` loop/1 ``` 。
173
225
174
- 执行一下试试 :
226
+ 执行一下 ``` iex kv.exs ``` :
175
227
``` elixir
176
228
iex> {:ok , pid}= KV .start
177
229
# PID<0.62.0>
182
234
```
183
235
184
236
一开始进程内的图变量是没有键值的,所以发送一个``` :get ``` 消息并且刷新当前进程的收件箱,返回nil。
185
- 下面再试试发送一个``` :put ``` 消息:
237
+ 下面发送一个``` :put ``` 消息再试一次:
238
+
186
239
``` elixir
187
240
iex> send pid, {:put ,:hello ,:world }
188
241
# PID<0.62.0>
@@ -192,10 +245,10 @@ iex> flush
192
245
:world
193
246
```
194
247
195
- 注意进程是怎么保持一个状态的:我们通过同该进程收发消息来获取和更新这个状态 。
196
- 事实上,任何进程只要知道该进程的PID ,都能读取和修改状态。
248
+ 注意这个进程是怎么保持状态信息的:我们通过向该进程发送消息来获取和更新状态 。
249
+ 事实上,其它任何进程只要知道这个进程的PID ,都能读取和修改状态。
197
250
198
- 还可以注册这个PID,给它一个名称。这使得人人都知道它的名字,并通过名字来向它发送消息 :
251
+ 还可以注册这个PID,给它一个名称。这使得我们可以通过名字来向它发送消息 :
199
252
``` elixir
200
253
iex> Process .register (pid,:kv )
201
254
true
@@ -205,9 +258,11 @@ iex> flush
205
258
:world
206
259
```
207
260
208
- 使用进程维护状态,以及注册进程都是Elixir程序非常常用的方式。
209
- 但是大多数时间我们不会自己实现,而是使用Elixir提供的抽象实现。
210
- 例如,Elixir提供的[ agent] ( http://elixir-lang.org/docs/stable/elixir/Agent.html ) 就是一种维护状态的简单的抽象实现:
261
+ 使用进程维护状态、进程名称注册都是构建Elixir应用的常见模式。
262
+ 但是大多数时间我们不会自己实现这样的模式,而是使用Elixir已经提供的抽象实现。
263
+
264
+ 例如,Elixir提供的[ agent] ( http://elixir-lang.org/docs/stable/elixir/Agent.html ) 就是一个状态维护进程的简单实现:
265
+
211
266
``` elixir
212
267
iex> {:ok , pid}= Agent .start_link (fn -> %{}end )
213
268
{:ok ,# PID<0.72.0>}
@@ -217,7 +272,9 @@ iex> Agent.get(pid, fn map -> Map.get(map, :hello) end)
217
272
:world
218
273
```
219
274
220
- 给``` Agent.start/2 ``` 方法加一个一个``` :name ``` 选项可以自动为其注册一个名字。
221
- 除了agents,Elixir还提供了创建通用服务器(generic servers,称作GenServer)、
222
- 通用时间管理器以及事件处理器(又称GenEvent)的API。
223
- 这些,连同“管理者”树,都可以在Mix和OTP手册里找到详细说明。
275
+ 给``` Agent.start/2 ``` 方法加上``` :name ``` 选项,可以自动为其注册一个名字。
276
+
277
+ 除了agents,Elixir还提供了一套API来创建通用服务器(generic servers,称作GenServer),任务等。
278
+ 这些都是建立在进程概念之上的实现。其它概念,包括“管理者”树,都可以在《高级》篇里找到更详细的说明。
279
+
280
+ 下一章将介绍Elixir语言的I/O世界。