按指定顺序累积消息输出

时间:2016-03-25 10:13:29

标签: elixir

我希望以指定的顺序累积不同进程发送的消息的输出。

例如,我有一个pid列表[pid0, pid1]。如果我先获得pid0,那么很好。如果我先获得pid1,那么在我获得pid0之前不会发生任何事情。

我知道这可以通过使用地图或关键字查找以及在收到所有消息后进行排序来解决。但是,这可以通过模式匹配来实现吗?

例如,写下这样的内容以保证它只是尝试接收消息input_pid(列表中的第一条消息):

defp loop([input_pid | remaining_input_pids], acc) do
  receive do
    {:forward, ^input_pid, output} -> loop(remaining_input_pids, [output | acc])
  end
end

2 个答案:

答案 0 :(得分:2)

让我们先看一个不起作用的例子,然后找到解决方法:

caller = self

pids = for x <- 1..100 do
  spawn fn ->
    :random.seed(:erlang.monotonic_time)
    :timer.sleep(:random.uniform(1000))
    send(caller, x)
  end
end

for _ <- pids do
  receive do
    x -> IO.inspect(x)
  end
end

这会产生100个进程,每个进程在将x发送回调用方之前会随机休眠一段时间。之后,结果按照发回的顺序收到,这将是随机的。

如果我们想按生成过程的顺序得到结果,我们需要给接收过程一个提示。正如您所正确观察到的那样,我们可以使用衍生进程“pid来实现这一点。我们从调用者中获取pid作为spawn的返回值,但我们也可以通过调用进程内的pid来获取生成进程的self,然后发送它回到来电者:

caller = self

pids = for x <- 1..100 do
  spawn fn ->
    :random.seed(:erlang.monotonic_time)
    :timer.sleep(:random.uniform(1000))
    send(caller, {self, x})
  end
end

for pid <- pids do
  receive do
    {^pid, x} -> IO.inspect(x)
  end
end

请注意,这可能会淹没调用者的进程收件箱。在最坏的情况下,首先产生的进程可能会被最后一次接收,然后所有其他消息将一直存在直到结束。

现在,您是否需要更进一步取决于流程的数量,您应该根据具体情况对结果进行基准测试。大量进程的问题在于如何处理邮箱:每次调用receive时,它都将遍历所有消息,直到找到匹配项。这意味着在最糟糕的情况下,您将遍历邮箱n + (n-1) + (n-2) + ... + 1 = n*(n+1)/2次(二次时间复杂度O(n²)),这可能成为瓶颈。

在这种情况下,更好的选择可能是立即接收消息,将它们存储在地图中,然后以正确的顺序读出它们。这样做的缺点是,总是必须等到收到所有消息。使用第一种方法,一旦下一个消息到达,就立即处理消息。这是一个如何做到这一点的例子。在这种情况下,我使用了100,000个进程,使用另一种方法已经非常慢:

caller = self

pids = for x <- 1..100_000 do
  spawn fn ->
    :random.seed(:erlang.monotonic_time)
    :timer.sleep(:random.uniform(1000))
    send(caller, {self, x})
  end
end

results = Enum.reduce pids, %{}, fn(_, results) ->
  receive do
    {pid, x} -> Map.put(results, pid, x)
  end
end

for pid <- pids do
  IO.inspect(results[pid])
end

答案 1 :(得分:0)

@ patrick-oscity给出了一个非常深入的答案,我非常喜欢它。但是,我已经使用了一个只能使用针对pin运算符进行模式匹配的版本。

对于上下文,我正在编写一个累加器,当收到所有输入时,它将所有输入的结果转发给执行器。我还希望累积输出(列表)遵守最初发送其输出的进程的预定义顺序。

执行此操作后,累加器将再次开始等待消息。

累加器如下所示:

def start_link(actuator, actuator_pid) do
  Task.start_link(fn -> loop(actuator, actuator_pid, actuator.inputs, []) end)
end

defp loop(actuator, actuator_pid, [], acc) do
  result = acc |> Enum.reverse
  send actuator_pid, {:forward, self(), result}
  loop(actuator, actuator_pid, actuator.inputs, [])
end

defp loop(actuator, actuator_pid, [input | remaining_inputs], acc) do
  receive do
    {:forward, ^input, output} ->
      loop(actuator, actuator_pid, remaining_inputs, [output | acc])
  end
end

为证明它符合我的要求,对此的测试如下:

test "When I recieve the outputs from all my inputs, then I return the accumulated result in the order of my actuator's inputs" do

{_, pid0} = Task.start_link(fn -> test_loop end)
{_, pid1} = Task.start_link(fn -> test_loop end)
{_, pid2} = Task.start_link(fn -> test_loop end)

actuator = %Cerebrum.Actuator{
  inputs: [pid0, pid1, pid2]
}

actuator_pid = self()
{_, pid} = start_link(actuator, actuator_pid)

send pid, {:forward, pid0, 0}
refute_receive {:forward, pid, [0]}

send pid, {:forward, pid1, 1}
refute_receive {:forward, pid, [0, 1]}

send pid, {:forward, pid2, 2}
assert_receive {:forward, pid, [0, 1, 2]}

send pid, {:forward, pid0, 0}
send pid, {:forward, pid1, 1}
send pid, {:forward, pid2, 2}
assert_receive {:forward, pid, [0, 1, 2]}

send pid, {:forward, pid0, 0}
send pid, {:forward, pid2, 2}
send pid, {:forward, pid1, 1}
assert_receive {:forward, pid, [0, 1, 2]}

send pid, {:forward, pid1, 1}
send pid, {:forward, pid0, 0}
send pid, {:forward, pid2, 2}
assert_receive {:forward, pid, [0, 1, 2]}

send pid, {:forward, pid1, 1}
send pid, {:forward, pid2, 2}
send pid, {:forward, pid0, 0}
assert_receive {:forward, pid, [0, 1, 2]}

send pid, {:forward, pid2, 2}
send pid, {:forward, pid1, 1}
send pid, {:forward, pid0, 0}
assert_receive {:forward, pid, [0, 1, 2]}

send pid, {:forward, pid2, 2}
send pid, {:forward, pid1, 1}
send pid, {:forward, pid0, 0}
assert_receive {:forward, pid, [0, 1, 2]}

end

defp test_loop do
  test_loop
end