Я создал много GenServer и отправляю им сообщения по одному, используя PID. Но я хочу, чтобы все они действовали примерно одновременно в один «ход» в игре. Как я могу: (1) транслировать "вперед!" сообщение для них (2) знаете, что все они закончили действовать (т.е. очередь окончена)?
... Программисты, придумавшие OTP, проделали всю тяжелую работу в этих сложных сценариях, и вы можете усилить их тяжелую работу, используя OTP. Если вы хотите перезапустить вышедший из строя GenServer и дать ему завершить свою работу, вы можете использовать OTP Supervisor. Тем не менее, это неплохое упражнение - пытаться делать что-то вручную, чтобы понять всю сложность. На каждой итерации вашего решения кто-нибудь может указать на крайние случаи и недостатки, которые вы не устраняете.
@ 7stud, я ценю этот совет и буду работать с OTP Supervisor. Прямо сейчас меня больше волнует время, чем сбой.
Один из способов добиться желаемого - это отправить cast
все сообщения go
, а затем ответить асинхронно:
defmodule TurnTracker do
use GenServer
def init(pids) do
state = %{
pids: pids,
ongoing_requests: MapSet.new()
}
{:ok, state}
end
# This will send out your go message to all genservers
def handle_call(:broadcast, _from, state) do
Enum.each(state.pids, fn pid ->
GenServer.cast(pid, {:go, self()})
end)
# The ongoing_requests is just a collection of all of the pids for the Player processes. One-by-one they will be removed using the handle_cast underneath here.
updated_state = Map.put(state, :ongoing_requests, MapSet.new(state.pids))
{:reply, :ok, updated_state}
end
# When one of the genservers is done its turn, it will send a message to here
def handle_cast({:completed_turn, pid}, state) do
# Remove the pid from the set, eventually we will remove them all
ongoing_requests = MapSet.delete(state.ongoing_requests, pid)
updated_state = Map.put(state, :ongoing_requests, ongoing_requests)
# Check to see if that was the last one, if it was, all of the Players are done their turns
if MapSet.size(ongoing_requests) == 0 do
# All of your GenServers are done
else
# One or more are not done yet
end
{:noreply, updated_state}
end
end
# You will have a bunch of these
defmodule Player do
use GenServer
def handle_cast({:go, turn_tracker_pid}, state) do
# Your turn logic here
# Tell the TurnTracker that we are done
GenServer.cast(turn_tracker_pid, {:completed_turn, self()})
{:noreply, state}
end
end
На самом деле нет способа гарантировать, что ваши GenServers будут действовать одновременно, потому что, когда вы отправляете сообщение, вы просто помещаете сообщение в их почтовые ящики, и перед вами могут быть другие сообщения.
Если повороты занимают более 5 секунд (тайм-аут по умолчанию для Genserver.call
), то здесь :broadcast
отключится.
Спасибо @Tyler. Я подумал, что может быть специальная функция для трансляции на несколько PID. Итерация по коллекции - знакомое решение. В вашем примере Player не регистрируется в TurnTracker, поэтому я не понимаю, как TurnTracker генерирует свой список PID в начале каждого хода.
Я добавил список pids
в init
TurnTracker, я решил, что вы можете передать их при запуске (так что сначала запустите все плееры).
Для простейшего решения я бы сделал сообщение GenServer
синхронным (используя call
вместо cast
для отправки сообщения) и запустил Task
для каждого сервера, чтобы дождаться результата (чтобы не блокировать исходный процесс вызова и иметь возможность запускать много сообщений одновременно). В основном примерно так:
servers
|> Enum.map(fn server -> Task.async(fn -> GenServer.call(server, :go) end) end)
|> Enum.map(&Task.await/1)
Обратите внимание, что Task.await
имеет тайм-аут по умолчанию, поэтому, если ваши ходы очень длинные, вы можете увеличить его.