#进程编程
1. `spawn` 2. `send`和`receive` 3. 连接 4. 任务 5. 状态
在Elixir中,全部代码都运行在进程内。进程相互独立,并发地运行,经过传送信息来交流。进程不是Elixir中惟一的并发基础,但它意味着可以构建分布式的,可容错的程序。服务器
Elixir中的进程不能和操做系统中的进程搞混。Elixir中的进程在内存和CPU占用上是极致的轻量级(不像其余编程语言中的线程)。所以,同时运行数万甚至数十万的进程也就不足为奇。并发
本章,咱们将学习用于生成进程的基础结构,还有在进程间收发信息。app
#spawn
async
生成进程的基础机制就是已经自动导入了的spawn/1
函数:编程语言
iex> spawn fn -> 1 + 2 end #PID<0.43.0>
spawn/1
会将一个函数放到另外一个进程中执行.分布式
注意spawn/1
返回了一个PID(进程标识).这时,你生成的进程已经濒死了.生成的进程会在执行完给定函数后退出:函数
iex> pid = spawn fn -> 1 + 2 end #PID<0.44.0> iex> Process.alive?(pid) false
注意:你获得的PID可能与例子不一样.工具
咱们能够经过self/0
获取当前进程的PID:oop
iex> self() #PID<0.41.0> iex> Process.alive?(self()) true
在咱们可以收发信息后,进程会变得有趣得多.
#send
和receive
咱们能够用send/2
发送信息给进程,并用receiver/1
接收:
iex> send self(), {:hello, "world"} {:hello, "world"} iex> receive do ...> {:hello, msg} -> msg ...> {:world, msg} -> "won't match" ...> end "world"
当一个信息传送至进程,它被存放在进程的邮箱中.receive/1
块会进入当前进程的邮箱,搜索是否有能模式匹配成功的信息.receive/1
支持卫语句和多从句,例如case/2
.
若是邮箱中没有可以匹配任何模式的信息,当前进程会一直等到可以匹配的信息出现.等待时间也能够被指定:
iex> receive do ...> {:hello, msg} -> msg ...> after ...> 1_000 -> "nothing after 1s" ...> end "nothing after 1s"
若是你想要的是已经在邮箱中的信息,能够将时限设置为0.
让咱们使用这些来在进程间通讯:
iex> parent = self() #PID<0.41.0> iex> spawn fn -> send(parent, {:hello, self()}) end #PID<0.48.0> iex> receive do ...> {:hello, pid} -> "Got hello from #{inspect pid}" ...> end "Got hello from #PID<0.48.0>"
当在用户界面中时,你会发现flush/0
助手很是有用.它会刷新并打印邮箱中的全部信息.
iex> send self(), :hello :hello iex> flush() :hello :ok
#连接
Elixir中最经常使用的生成方式就是spawn_link/1
.在咱们展现spawn_link/1
的例子以前,先看看当进程失败时会发生什么:
iex> spawn fn -> raise "oops" end #PID<0.58.0> [error] Process #PID<0.58.00> raised an exception ** (RuntimeError) oops :erlang.apply/2
仅仅是记录了错误,进程依然可以被生成.这是由于进程之间是独立的.若是咱们但愿一个进程的失败影响到其它进程,咱们就须要连接它们.使用spawn_link/1
:
iex> spawn_link fn -> raise "oops" end #PID<0.41.0> ** (EXIT from #PID<0.41.0>) an exception was raised: ** (RuntimeError) oops :erlang.apply/2
当壳中发生了一个错误,壳会自动捕获这个错误并以良好的格式展现出来.为了理解咱们的代码中究竟发生了什么,让咱们在文件中来使用spawn_link/1
并运行它:
# spawn.exs spawn_link fn -> raise "oops" end receive do :hello -> "let's wait until the process fails" end
$ elixir spawn.exs ** (EXIT from #PID<0.47.0>) an exception was raised: ** (RuntimeError) oops spawn.exs:1: anonymous fn/0 in :elixir_compiler_0.__FILE__/1
这一次进程失败了而且关闭了它所连接的父进程.咱们也能够经过调用Process.link/1
来手工连接进程.咱们建议你查看一下Process
模块,其中有进程的其余功能.
进程和连接在建立可容错系统中扮演着重要角色.在Elixir应用中,咱们常常将进程和管理者连接起来,管理者的做用是监督这块区域中进程的生死.进程间是独立的且默认不分享任何东西,因此它们不会毁坏或影响其它进程.
不一样于其它语言要求咱们捕捉/处理异常,在Elixir中咱们能够任由进程失败,由于咱们指望管理者能合适地重启咱们的系统."快速失败"是编写Elixir软件时的一条守则.
spawn/1
和spawn_link/1
是Elixir里用于建立进程的最原始的方法.尽管咱们目前只用过它们,但大多数时候咱们将抽象地在它们的上层操做.让咱们来看看最经常使用的方式--任务.
#任务
任务创建在生成函数的上层,提供了更好的错误报告的检讨机制:
iex(1)> Task.start fn -> raise "oops" end {:ok, #PID<0.55.0>} 15:22:33.046 [error] Task #PID<0.55.0> started from #PID<0.53.0> terminating ** (RuntimeError) oops (elixir) lib/task/supervised.ex:74: Task.Supervised.do_apply/2 (stdlib) proc_lib.erl:239: :proc_lib.init_p_do_apply/3 Function: #Function<20.90072148/0 in :erl_eval.expr/5> Args: []
与spawn/1
和spawn_link/1
不一样的是,咱们用Task.start/1
和Task.start_link/1
时会返回{:ok, pid}
,而不是只有PID.这使得任务能够被用于管理者树上.Task
提供了诸如Task.async/1
和Task.await/1
这样的便捷函数,以及缓解分布性的功能.
咱们将在Mix和OTP介绍中探索这些功能,如今只须要记住任务提供了更好的错误报告.
#状态
目前咱们尚未讲过状态.若是你的应用须要状态,好比说,来保存你的应用设置,或你须要解读一个文件并保存在内存中,你该如何存放它们?
最一般的答案是进程.咱们能够编写一个无限循环的进程来保存状态,收发信息.例如,让咱们来编写一个模块,内容是开启一个像键值对同样运做的进程,存放在kv.exs
文件中:
defmodule KV do def start_link do Task.start_link(fn -> loop(%{}) end) end defp loop(map) do receive do {:get, key, caller} -> send caller, Map.get(map, key) loop(map) {:put, key, value} -> loop(Map.put(map, key, value)) end end end
注意start_link
函数开启了一个运行loop/1
函数的新进程,参数是一个空映射.loop/1
函数等待着信息,而且对每一个信息作出合适的反应.当匹配到:get
信息时,它会反馈给调用者一个信息并再次调用loop/1
,等待新的信息.当:put
信息以新版本的映射调用了loop/1
时,给定的key
和value
就被存储了.
让咱们试着运行iex kv.exs
:
iex> {:ok, pid} = KV.start_link #PID<0.62.0> iex> send pid, {:get, :hello, self()} {:get, :hello, #PID<0.41.0>} iex> flush nil :ok
一开始,进程映射中没有键,因此发送一个:get
信息,而后刷新当前进程的收件箱会获得nil
.让咱们发送一个:put
信息并再试一次:
iex> send pid, {:put, :hello, :world} {:put, :hello, :world} iex> send pid, {:get, :hello, self()} {:get, :hello, #PID<0.41.0>} iex> flush :world :ok
注意进程是如何保存状态的,以及咱们能够经过向进程发送信息来获取和更新这个状态.事实上咱们能够向任何已知pid
的进程发送信息并操做状态.
也能够用一个名字注册pid
,并容许任何知道这个名字的人发送信息给它:
iex> Process.register(pid, :kv) true iex> send :kv, {:get, :hello, self()} {:get, :hello, #PID<0.41.0>} iex> flush :world :ok
使用进程存放状态,以及名字注册在Elixir中都是很是广泛的模式.然而,一般咱们不会像上面那样手工操做这些模式,而是使用Elixir装载的一些抽象工具.例如,Elixir提供了代理,它是状态的一种简单抽象:
iex> {:ok, pid} = Agent.start_link(fn -> %{} end) {:ok, #PID<0.72.0>} iex> Agent.update(pid, fn map -> Map.put(map, :hello, :world) end) :ok iex> Agent.get(pid, fn map -> Map.get(map, :hello) end) :world
Agent.start_link/2
能够设置:name
选项,而且会自动注册.除了代理,Elixir还提供了用于建立通用服务器(GenServer),任务等等的API,它们的底层都是由进程支持的.咱们将在Mix和OTP入门中沿着管理者树仔细探索,同时从头至尾建立一个完整的Elixir应用.
接下来让咱们开始探索Elixir中的I/O世界.