Elixir
(およびもちろんもちろんErlang
)のプロセスは 、 一意のプロセス識別子 -pidを使用して識別されます。
これらを使用してプロセスとやり取りします。 メッセージはpid
ように送信され、仮想マシン自体がこれらのメッセージを正しいプロセスに配信します。
ただし、場合によっては、 pid
に対する過度の信頼が重大な問題につながる可能性があります。
たとえば、死んだプロセスのpid
保存したり、 pid
作成を抽象化するSupervisor
を使用したりできます。したがって、どのpid
を持っているかさえわかりません( per :また、 Supervisor
では、別のpid
プロセスを再起動できます。これについては一切知りません)。
簡単なアプリケーションを作成して、直面する可能性のある問題と、これらの問題をどのように解決するかを見てみましょう。
レジストリなしで始める
最初の例では、簡単なチャットを作成します。 mix
プロジェクトを作成することから始めましょう。
$ mix new chat
この記事のすべての例で使用する、絶対に標準的なGenServer
作成しましょう。
# ./lib/chat/server.ex defmodule Chat.Server do use GenServer # API def start_link do GenServer.start_link(__MODULE__, []) end def add_message(pid, message) do GenServer.cast(pid, {:add_message, message}) end def get_messages(pid) do GenServer.call(pid, :get_messages) end # SERVER def init(messages) do {:ok, messages} end def handle_cast({:add_message, new_message}, messages) do {:noreply, [new_message | messages]} end def handle_call(:get_messages, _from, messages) do {:reply, messages, messages} end end
そのようなコードがなじみのない、または理解できないと思われる場合は、OTPに関する優れたパラグラフが記載されているElixir
の作業開始をお読みください。
mix
環境でiex
セッションをiex
し、サーバーでの作業を試みましょう。
$ iex -S mix iex> {:ok, pid} = Chat.Server.start_link {:ok, #PID<0.107.0>} iex> Chat.Server.add_message(pid, "foo") :ok iex> Chat.Server.add_message(pid, "bar") :ok iex> Chat.Server.get_messages(pid) ["bar", "foo"]
このステップのコードはこのコミットにあります。
この段階では、すべてが素晴らしく、素晴らしいだけです。 pid
を取得し、送信する各メッセージ( add_message/2
およびget_messages/1
)に対してこのpid
を渡します。すべてが予想get_messages/1
機能し、退屈さえします。
しかし、 Supervisor
を追加しようとすると楽しみが始まります...
とてもいい:私はSupervisor
です!
そのため、何らかの理由でChat.Server
プロセスChat.Server
死に絶えています。 私たちは空のコールドiex
セッションにiex
ており、新しいプロセスを開始し、そのpid
を取得し、この新しいpid
メッセージを書き込む以外に選択肢はありません。 Supervisor
作成しましょう-このような些細なことを心配する必要はありません!
# ./lib/chat/supervisor.ex defmodule Chat.Supervisor do use Supervisor def start_link do Supervisor.start_link(__MODULE__, []) end def init(_) do children = [ worker(Chat.Server, []) ] supervise(children, strategy: :one_for_one) end end
さて、 Supervisor
作成Supervisor
非常に簡単です。 しかし、サーバーの動作モデルが変わらない場合、問題が発生します。 結局のところ、 Chat.Server
プロセスを自分で開始するのではなく、 Supervisor
がこれを行ってくれます。 したがって、 pid
プロセスにはアクセスできません。
これはバグではなく、 Supervisor
などのOTP
パターンの機能です。 子プロセスのpid
アクセスすることはできません。予期せず(ただし、もちろん必要な場合のみ)プロセスを再起動できますが、実際にはプロセスをpid
し、新しいpid
新しいpid
を作成します。
プロセス名を登録する
Chat.Server
プロセスChat.Server
アクセスするには、プロセスを指す方法を考え出す必要があります。もう一方はpid
はありません。 Supervisor
を介してプロセスが再起動された場合でも(つまり、 pid
が変更された場合でも)ポインターが保存されるように、ポインターが必要です。
そして、そのようなポインターは
と呼ばれます!
まず、 Chat.Server
変更しChat.Server
。
# ./lib/chat/server.ex defmodule Chat.Server do use GenServer def start_link do # We now start the GenServer with a `name` option. GenServer.start_link(__MODULE__, [], name: :chat_room) end # And our function doesn't need to receive the pid anymore, # as we can reference the process with its unique name. def add_message(message) do GenServer.cast(:chat_room, {:add_message, message}) end def get_messages do GenServer.call(:chat_room, :get_messages) end # ... end
変更-このコミットで
これですべてが同じように機能するはずですが、より良いだけです-このpid
どこにでも渡さないでください:
$ iex -S mix iex> Chat.Supervisor.start_link {:ok, #PID<0.94.0>} iex> Chat.Server.add_message("foo") :ok iex> Chat.Server.add_message("bar") :ok iex> Chat.Server.get_messages ["bar", "foo"]
プロセスが再起動しても、同じ方法でアクセスできます。
iex> Process.whereis(:chat_room) #PID<0.111.0> iex> Process.whereis(:chat_room) |> Process.exit(:kill) true iex> Process.whereis(:chat_room) #PID<0.114.0> iex> Chat.Server.add_message "foo" :ok iex> Chat.Server.get_messages ["foo"]
さて、私たちの現在のタスクについては、問題は解決されているように見えますが、もっと複雑な(そして実際のタスクにより近い)ことを試してみましょう。
動的プロセス作成
複数のチャットルームをサポートする必要があるとします。 クライアントは名前を持つ新しいルームを作成でき、希望するルームにメッセージを送信できることを期待しています。 その場合、インターフェースは次のようになります。
iex> Chat.Supervisor.start_room("first room") iex> Chat.Supervisor.start_room("second room") iex> Chat.Server.add_message("first room", "foo") iex> Chat.Server.add_message("second room", "bar") iex> Chat.Server.get_messages("first room") ["foo"] iex> Chat.Server.get_messages("second room") ["bar"]
上から始めて、これをすべてサポートするようにSupervisor
を変更しましょう。
# ./lib/chat/supervisor.ex defmodule Chat.Supervisor do use Supervisor def start_link do # We are now registering our supervisor process with a name # so we can reference it in the `start_room/1` function Supervisor.start_link(__MODULE__, [], name: :chat_supervisor) end def start_room(name) do # And we use `start_child/2` to start a new Chat.Server process Supervisor.start_child(:chat_supervisor, [name]) end def init(_) do children = [ worker(Chat.Server, []) ] # We also changed the `strategty` to `simple_one_for_one`. # With this strategy, we define just a "template" for a child, # no process is started during the Supervisor initialization, # just when we call `start_child/2` supervise(children, strategy: :simple_one_for_one) end end
そして、 start_link
関数で名前を受け入れるようにしましょう:
# ./lib/chat/server.ex defmodule Chat.Server do use GenServer # Just accept a `name` parameter here for now def start_link(name) do GenServer.start_link(__MODULE__, [], name: :chat_room) end #... end
変更-このコミットで
そして、ここに問題があります! 複数のChat.Server
プロセスを使用できますが、それらすべてに:chat_room
という名前を:chat_room
ことはできません。 トラブル...
$ iex -S mix iex> Chat.Supervisor.start_link {:ok, #PID<0.107.0>} iex> Chat.Supervisor.start_room "foo" {:ok, #PID<0.109.0>} iex> Chat.Supervisor.start_room "bar" {:error, {:already_started, #PID<0.109.0>}}
正直なところ、 VM
非常に雄弁です。 2番目のプロセスを作成しようとしていますが、同じ名前のプロセスが既に存在しているため、環境から非常に悪意があります。 他の方法を考え出す必要がありますが、どれですか?..
残念ながら、 name
引数のタイプは明確に定義されています。 {:chat_room, "room name"}
ようなものは使用できません。 ドキュメントを見てみましょう:
サポートされる値:
GenServer
この場合、GenServer
Process.register/2
を使用して、指定されたatom
名でローカルに登録されます。
{:global, term}
-この場合、GenServer
、:global
モジュールの関数を使用して、指定されたterm
名でグローバルに登録されます。
{:via, module, term}
-この場合、GenServer
module
で定義されたメカニズムと名前「term」を使用して登録されmodule
。
サポートされている値は次のとおりです。
atom
GenServer
は、Process.register/2
を使用して、指定された名前でローカルに登録されます。
{:global, term}
GenServer
は、:global
モジュールの関数を使用して、指定された用語でグローバルに登録されます。
{:via, module, term}
GenServer
は指定されたメカニズムと名前で登録されます。
最初のオプションはatom
、すでに使用しています。トリッキーなケースでは収まらないことは確かです。
2番目のオプションは、プロセスをノードクラスターにグローバルに登録するために使用されます。 ローカルETS
テーブルを使用します。 さらに、クラスター内のノード内で一定の同期が必要になるため、プログラムの速度が低下します。 したがって、 本当に必要な場合にのみ使用してください。
3番目の最後のオプションは、 :via
をパラメーターとして使用するタプルを使用します。これは、まさに問題を解決するために必要なものです。 これについてはドキュメントに次のように書かれています:
オプション:via
は、register_name/2
、unregister_name/1
、whereis_name/1
およびsend/2
インターフェースを持つモジュールをパラメーターとして受け入れます。
:viaオプションでは、register_name / 2、unregister_name / 1、whereis_name / 1、send / 2をエクスポートするモジュールが必要です。
何もはっきりしていませんか? 私も! それでは、このメソッドの動作を見てみましょう。
タプルの使用:via
したがって、タプル:via
は、 Elixir
プロセスを登録するために別のモジュールを使用することを伝える方法です。 このモジュールは次のことを行う必要があります。
-
register_name/2
関数を使用して、任意のterm
名前を登録します。 -
unregister_name/1
関数を使用して、レジスタから名前を削除します。 -
whereis_name/1
を使用して名前でpid
を見つけます。 -
send/2
を使用して特定のプロセスにメッセージを送信します。
これが機能するためには、上記の関数はOTP
定義された特定の形式で応答を送信する必要がありhandle_call/3
およびhandle_cast/2
特定のルールに従うように。
これをすべて知っているモジュールを定義してみましょう:
# ./lib/chat/registry.ex defmodule Chat.Registry do use GenServer # API def start_link do # We register our registry (yeah, I know) with a simple name, # just so we can reference it in the other functions. GenServer.start_link(__MODULE__, nil, name: :registry) end def whereis_name(room_name) do GenServer.call(:registry, {:whereis_name, room_name}) end def register_name(room_name, pid) do GenServer.call(:registry, {:register_name, room_name, pid}) end def unregister_name(room_name) do GenServer.cast(:registry, {:unregister_name, room_name}) end def send(room_name, message) do # If we try to send a message to a process # that is not registered, we return a tuple in the format # {:badarg, {process_name, error_message}}. # Otherwise, we just forward the message to the pid of this # room. case whereis_name(room_name) do :undefined -> {:badarg, {room_name, message}} pid -> Kernel.send(pid, message) pid end end # SERVER def init(_) do # We will use a simple Map to store our processes in # the format %{"room name" => pid} {:ok, Map.new} end def handle_call({:whereis_name, room_name}, _from, state) do {:reply, Map.get(state, room_name, :undefined), state} end def handle_call({:register_name, room_name, pid}, _from, state) do # Registering a name is just a matter of putting it in our Map. # Our response tuple include a `:no` or `:yes` indicating if # the process was included or if it was already present. case Map.get(state, room_name) do nil -> {:reply, :yes, Map.put(state, room_name, pid)} _ -> {:reply, :no, state} end end def handle_cast({:unregister_name, room_name}, state) do # And unregistering is as simple as deleting an entry # from our Map {:noreply, Map.delete(state, room_name)} end end
繰り返しますが、レジストリが内部でどのように機能するかを選択してください。 ここでは、単純なMap
を使用して、名前とpid
を関連付けます。 このコードは、 GenServer
どのようにGenServer
するかをよく知っている場合は特に、絶対にシンプルで簡単です。 関数によって返される値のみが不慣れに見える場合があります。
iex
セッションでレジストリを試す時がiex
ました:
$ iex -S mix iex> {:ok, pid} = Chat.Server.start_link("room1") {:ok, #PID<0.107.0>} iex> Chat.Registry.start_link {:ok, #PID<0.109.0>} iex> Chat.Registry.whereis_name("room1") :undefined iex> Chat.Registry.register_name("room1", pid) :yes iex> Chat.Registry.register_name("room1", pid) :no iex> Chat.Registry.whereis_name("room1") #PID<0.107.0> iex> Chat.Registry.unregister_name("room1") :ok iex> Chat.Registry.whereis_name("room1") :undefined
5秒-素晴らしいフライト! レジストリは正常に機能します。レジストリを登録および削除します。 チャットで使用してみましょう。
問題は、複数のChat.Server
サーバーがChat.Server
、 Supervisor
で初期化されたことでした。 特定のルームにメッセージを送信するには、 Chat.Server.add_message(“room1”, “my message”)
を呼び出したいので、サーバー名を{:chat_room, “room1”}
および{:chat_room, “room2”}
として登録する必要があります{:chat_room, “room2”}
タプルを介して行う方法は次のとおりです。
# ./lib/chat/server.ex defmodule Chat.Server do use GenServer # API def start_link(name) do # Instead of passing an atom to the `name` option, we send # a tuple. Here we extract this tuple to a private method # called `via_tuple` that can be reused in every function GenServer.start_link(__MODULE__, [], name: via_tuple(name)) end def add_message(room_name, message) do # And the `GenServer` callbacks will accept this tuple the # same way it accepts a pid or an atom. GenServer.cast(via_tuple(room_name), {:add_message, message}) end def get_messages(room_name) do GenServer.call(via_tuple(room_name), :get_messages) end defp via_tuple(room_name) do # And the tuple always follow the same format: # {:via, module_name, term} {:via, Chat.Registry, {:chat_room, room_name}} end # SERVER (no changes required here) # ... end
変更-このコミットで
ここで何が起こるかです: Chat.Server
にメッセージを送信してルームの名前を渡すChat.Server
に、タプルで渡されたモジュールを使用してpid
このpid
見つけます:via
(この場合はChat.Registry
) 。
これで問題が解決しますChat.Server
プロセスをいくつでも使用できるようになり(名前の空想が終わるまで)、そのpid
を知る必要がなくなりました。 絶対に。
ただし、このソリューションには別の問題があります。 推測?
まさに! レジストリは、クラッシュしたプロセスを認識していないため、 Supervisor
使用して再起動する必要があります。 これは、これが発生すると、レジストリは同じ名前のレコードを再作成することを許可せずpid
デッドプロセスのpid
保存することを意味します。
理論的には、この問題の解決策はそれほど複雑ではありません。 レジストリに、 pid
が保存されているすべてのプロセスを監視するよう強制します。 そのような「観察可能な」プロセスが落ちたらすぐに、レジストリから削除します。
# in lib/chat/registry.ex defmodule Chat.Registry do # ... def handle_call({:register_name, room_name, pid}, _from, state) do case Map.get(state, room_name) do nil -> # When a new process is registered, we start monitoring it. Process.monitor(pid) {:reply, :yes, Map.put(state, room_name, pid)} _ -> {:reply, :no, state} end end def handle_info({:DOWN, _, :process, pid, _}, state) do # When a monitored process dies, we will receive a # `:DOWN` message that we can use to remove the # dead pid from our registry. {:noreply, remove_pid(state, pid)} end def remove_pid(state, pid_to_remove) do # And here we just filter out the dead pid remove = fn {_key, pid} -> pid != pid_to_remove end Enum.filter(state, remove) |> Enum.into(%{}) end end
変更-このコミットで
すべてが機能することを確認します。
$ iex -S mix iex> Chat.Registry.start_link {:ok, #PID<0.107.0>} iex> Chat.Supervisor.start_link {:ok, #PID<0.109.0>} iex> Chat.Supervisor.start_room("room1") {:ok, #PID<0.111.0>} iex> Chat.Server.add_message("room1", "message") :ok iex> Chat.Server.get_messages("room1") ["message"] iex> Chat.Registry.whereis_name({:chat_room, "room1"}) |> Process.exit(:kill) true iex> Chat.Server.add_message("room1", "message") :ok iex> Chat.Server.get_messages("room1") ["message"]
さて、 Supervisor
がChat.Server
プロセスを再起動する回数はまったくChat.Server
ルームにメッセージを送信するとすぐに、正しいpid
配信されます。
gproc
簡素化する
原則として、チャットは終了しますが、タプルを使用して登録を簡素化するもう1つの機能について説明:via
ます。 これはgproc
ライブラリであるgproc
です。
そして、 gproc
代わりにChat.Server
使用するようgproc
に教えてChat.Server
、 Chat.Server
完全にChat.Registry
ます。
依存関係から始めましょう。 これを行うには、 gproc
をmix.exs
に追加しmix.exs
。
# ./mix.exs defmodule Chat.Mixfile do # ... def application do [applications: [:logger, :gproc]] end defp deps do [{:gproc, "0.3.1"}] end end
次に、依存関係を次のようにプルアップします。
$ mix deps.get
タプルを使用して登録を変更できます:via
- gproc
ではなくChat.Registry
使用します:
# ./lib/chat/server.ex defmodule Chat.Server do # ... # The only thing we need to change is the `via_tuple/1` function, # to make it use `gproc` instead of `Chat.Registry` defp via_tuple(room_name) do {:via, :gproc, {:n, :l, {:chat_room, room_name}}} end # ... end
gproc
は{type, scope, key}
の3つの値で構成されるgproc
使用します。
この場合、以下を使用します。
-
:n
これは
意味し
。つまり、そのようなキーの下に複数のプロセスを登録することはできません。 -
:l
これはlocal
意味します。つまり、プロセスはノードでのみ登録されます。 -
{:chat_room, room_name}
は、タプル形式のキーそのものです。
可能なgproc
設定の詳細については、 こちらをご覧ください 。
このような変更の後、 iex
し、 iex
セッションですべてが引き続き機能することを確認します。
$ iex -S mix iex> Chat.Supervisor.start_link {:ok, #PID<0.190.0>} iex> Chat.Supervisor.start_room("room1") {:ok, #PID<0.192.0>} iex> Chat.Supervisor.start_room("room2") {:ok, #PID<0.194.0>} iex> Chat.Server.add_message("room1", "first message") :ok iex> Chat.Server.add_message("room2", "second message") :ok iex> Chat.Server.get_messages("room1") ["first message"] iex> Chat.Server.get_messages("room2") ["second message"] iex> :gproc.where({:n, :l, {:chat_room, "room1"}}) |> Process.exit(:kill) true iex> Chat.Server.add_message("room1", "first message") :ok iex> Chat.Server.get_messages("room1") ["first message"]
変更-このコミットで
次はどこ?
あなたと私はたくさんの複雑な質問を見つけました。 主な調査結果:
-
pid
直接操作する場合は注意してください。プロセスが再起動するとすぐに変更されます。 - 1つのプロセスのみへのリンクを取得する必要がある場合(単一のチャットルームの場合のように)、プロセスをアトム形式の名前で登録するだけで十分です。
- プロセスを動的に作成する必要がある場合(多くのチャットルーム)、タプル
:via
を使用して独自のレジストリを提供できます。 - 同様の
gproc
(gproc
)がすでに存在し、それらを使用する場合、自転車を構築する必要はありません。
もちろん、これだけではありません。 クラスタ内のすべてのノードでグローバル登録が必要な場合は、他のツールも有効です。 Erlang
は、グローバル登録用のグローバルモジュール、プロセスグループ用のgprc
があり、同じgprc
が役立ちます。
この記事に興味がある場合は、 Saša Jurić. Elixir in Action
をお読みくださいSaša Jurić. Elixir in Action
Saša Jurić. Elixir in Action
。