使用 elixir 处理多个进程之间的状态
Elixir非常适合并发代码,因为它功能强大,并且支持多进程运行。但是,当我们的代码在多个进程中运行时,我们该如何处理状态呢?其实有一些技巧,在本文中,我们将一起学习更多,好吗?
目录
什么是进程?如何在 send 和 receive 中使用进程
进程是 Elixir 对并发编程的回应;它们本质上是一个持续运行的节点,可以发送和接收消息。实际上,Elixir 中的每个函数都在一个进程内运行。虽然这听起来很昂贵,但与其他语言中的线程相比,它非常轻量级,这使得我们开发人员能够构建具有数百个进程同时运行的、高度可扩展的软件。在 Elixir 语言中使用进程的另一个巨大优势是,该语言建立在不变性和其他函数式编程概念之上,因此我们可以相信这些函数完全独立地运行,并且不会更改或维护全局状态。
查看进程运行的基本方式是使用spawn
函数,通过该函数我们可以在进程中执行函数并获取其pid 。
iex(3)> pid = spawn(fn -> IO.puts("teste") end)
teste
#PID<0.111.0>
iex(4)> pid
#PID<0.111.0>
iex(5)> Process.alive?(pid)
false
iex(6)>
从返回结果可以看出,Process.alive?(pid)
这个进程一旦正确运行就已经死了,但是我们可以很容易地添加一个睡眠函数来检查这个机制:
iex(2)> pid = spawn(fn -> :timer.sleep(10000); IO.puts("teste") end)
#PID<0.111.0>
iex(3)> Process.alive?(pid)
true
teste
iex(4)> Process.alive?(pid)
false
iex(5)>
由于我们睡眠了 10 秒,所以进程一直处于活动状态,直到 sleep 函数执行完毕并终止。很酷吧?重要的是要知道,我们的主程序并没有挂起,它只是将函数放入进程中,然后就忘了它的存在。这使我们能够创建真正模块化且性能卓越的代码,并在多个节点上运行。
除了在进程中生成函数之外,我们还可以使用函数send
和receive
块在进程之间转换信息,如下所示:
iex(1)> defmodule Listener do
...(1)> def call do
...(1)> receive do
...(1)> {:hello, msg} -> IO.puts("Received: #{msg}")
...(1)> end
...(1)> end
...(1)> end
{:module, Listener,
<<70, 79, 82, 49, 0, 0, 6, 116, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 240,
0, 0, 0, 25, 15, 69, 108, 105, 120, 105, 114, 46, 76, 105, 115, 116, 101,
110, 101, 114, 8, 95, 95, 105, 110, 102, 111, ...>>, {:call, 0}}
iex(2)> pid = spawn(&Listener.call/0)
#PID<0.115.0>
iex(3)> send(pid, {:hello, "Hello World"})
Received: Hello World
{:hello, "Hello World"}
iex(4)>
注意到我们使用了代码块定义了一个充当通用监听器的函数receive
。它就像一个 switch case,我们可以在其中进行模式匹配并执行快速操作,在本例中,我们只是将数据打印到STDOUT。一旦我们创建了这个监听器,就可以使用返回值,使用一个以 a和 a 为参数的函数pid
来发送信息。send/2
PID
这样,就可以在诸如 elixir 之类的不可变且独立的环境中保持状态。
增加我们的任务经验
Task 模块在函数之上提供了抽象,spawn
同时添加了对异步行为的支持,例如,在单独的进程中创建一个函数,并使用等待函数观察其行为。随着你深入 Elixir,你会发现该Task
模块允许你启动一个执行函数并返回任务结构体的新进程。有了这个结构体,你就可以轻松地使用子句从该函数中获取值Task.await(task)
,如下所示:
iex(1)> task = Task.async(fn ->
...(1)> IO.puts("Task is running")
...(1)> 42
...(1)> end)
Task is running
%Task{
mfa: {:erlang, :apply, 2},
owner: #PID<0.109.0>,
pid: #PID<0.110.0>,
ref: #Reference<0.0.13955.659691257.723058689.43945>
}
iex(2)> IO.puts "a code"
a code
:ok
iex(3)> answer_to_everything = Task.await(task)
42
iex(4)> answer_to_everything
42
iex(5)>
首先,我们看到Task is running
打印出来的消息,然后我们得到了任务结构体。此外,我们可以执行其间的任意代码,当我们准备就绪时,只需使用该Task.await
函数来获取函数返回值即可。
Task 模块还为常规spawn
函数提供了一个通用接口start
,我们甚至可以使用新的模块抽象重用开头显示的代码:
iex(1)> defmodule Listener do
...(1)> def call do
...(1)> receive do
...(1)> {:print, msg} -> IO.puts("Received message: #{msg}")
...(1)> end
...(1)> end
...(1)> end
{:module, Listener,
<<70, 79, 82, 49, 0, 0, 6, 244, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 245,
0, 0, 0, 26, 15, 69, 108, 105, 120, 105, 114, 46, 76, 105, 115, 116, 101,
110, 101, 114, 8, 95, 95, 105, 110, 102, 111, ...>>, {:call, 0}}
iex(2)> {:ok, pid} = Task.start(&Listener.call/0)
{:ok, #PID<0.115.0>}
iex(3)> send(pid, {:print, "Eat more fruits"})
Received message: Eat more fruits
{:print, "Eat more fruits"}
使用模块很有用Task
,因为我们可以获得更高层次的抽象。你一定注意到了Task.start
和 的接口是一样的,对吧?没错,我们可以交换它们,从而获得使用和 的Task.async
能力,这就是抽象低级概念的力量!Task.await
Task.yield
使用代理包装器设计状态
该Agent
模块提供了另一层抽象,专注于控制流程的多个实例之间的状态,它就像一个用于长期运行交互的数据结构。
我们可以首先使用从函数返回传递的初始值来启动代理实例,如下所示:
iex(1)> {:ok, agent} = Agent.start_link(fn -> [] end)
{:ok, #PID<0.110.0>}
iex(2)> agent
#PID<0.110.0>
iex(3)>
正如您所看到的,我们得到了PID
与其他抽象一样的结果,这里的区别可以在其他方法的使用中观察到。
例如,我们可以通过向原始数组附加一个值来更新它:
iex(3)> Agent.update(agent, fn list -> ["elixir" | list] end)
:ok
iex(4)>
这就是 Agent 模块提供的抽象的全部区别,我们可以通过将不可变函数附加为回调并重复使用来不断更新状态PID
。
我们还可以使用以下函数从数据结构返回特定值:
iex(4)> Agent.get(agent, fn list -> list end)
["elixir"]
iex(5)>
看到了吗?这就像从回调函数返回整个列表一样简单,你可以想象,如果需要,可以使用 Elixir 中的任何方法来过滤这个列表,并继续迭代数据结构。
结论
这只是对这个对我来说很新概念的简单介绍,希望对大家有所帮助!在接下来的文章中,我们将深入探讨 elixir 的其他主题,例如 Gen Servers、Supervisors 等等。愿原力与你同在!🍒
文章来源:https://dev.to/cherryramatis/handling-state- Between-multiple-instances-with-elixir-4jm1