5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GenServer でデッドロックを起こしてみる

5
Last updated at Posted at 2025-12-20

GenServer(単体)はメッセージを逐次処理します。

たとえば、GerServer A は GenServer B や C から並行にメッセージを投げられても、そのメッセージによってトリガされる A の処理である handle_call/3, handle_cast/2 はかならず逐次的に行われます。

Erlang/Elixir はこの仕様があるため、GenServer を実装する人が排他処理を GenServer 内部で書く必要は無いわけです。Erlang/Elixir が並行処理を書きやすいといわしめる仕様の一つだと思います。

並行に行われる動作をハンドリングするために排他処理を自分で組む場合に気をつけなればいけないことに、デッドロックがあると思います。GenServer 単体であれば、上記、仕様によって排他処理の実装を GenServer に任せる(我々は実装しない)ために、実装ミスによるデッドロックはそもそも起きません。では、 GenServer 複数の場合はどうでしょうか?

これは簡単にデッドロックを起こさせることができます。ぴゃっと書いていきましょう。

複数 GenServer でデッドロックを起こしてみる

mix new dead_lock --sup して、 GenServer を書きます。 DeadLock.Worker.test/1 を呼び出すと別の DeadLock.Workertest/1 を呼び出すという簡単なものです。

lib/dead_lock/worker.ex
defmodule DeadLock.Worker do
  use GenServer

  require Logger

  def test(name) do
    GenServer.call(name, :test)
  end

  def start_link(args) do
    name = Keyword.get(args, :name, __MODULE__)
    GenServer.start_link(__MODULE__, args, name: name)
  end

  def init(args) do
    name = Keyword.fetch!(args, :name)
    callee = Keyword.fetch!(args, :callee)

    {:ok, %{name: name, callee: callee}}
  end

  def handle_call(:test, _from, %{name: name, callee: callee} = state) do
    Logger.info("#{name} is called, then call #{callee}.")
    {:reply, __MODULE__.test(state.callee), state}
  end
end

これを Application の supervisor に、名前をつけて2つぶら下げてみます。
名前は A と B です。

lib/dead_lock/application.ex
defmodule DeadLock.Application do
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    children = [
      Supervisor.child_spec({DeadLock.Worker, [name: A, callee: B]}, id: A),
      Supervisor.child_spec({DeadLock.Worker, [name: B, callee: A]}, id: B)
    ]

    opts = [strategy: :one_for_one, name: DeadLock.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

iex -S mixdead_lock app を起動し、 DeadLock.Worker.test(A) 実行します。

iex(1)> DeadLock.Worker.test(A)

16:24:29.203 [info] Elixir.A is called, then call Elixir.B.

16:24:29.207 [info] Elixir.B is called, then call Elixir.A.
** (exit) exited in: GenServer.call(A, :test, 5000)
    ** (EXIT) time out
    (elixir 1.19.4) lib/gen_server.ex:1142: GenServer.call/3
    iex:1: (file)

16:24:34.211 [error] GenServer A terminating
** (stop) exited in: GenServer.call(B, :test, 5000)
    ** (EXIT) time out
    (elixir 1.19.4) lib/gen_server.ex:1142: GenServer.call/3
    (dead_lock 0.1.0) lib/dead_lock/worker.ex:24: DeadLock.Worker.handle_call/3
    (stdlib 7.1) gen_server.erl:2470: :gen_server.try_handle_call/4
    (stdlib 7.1) gen_server.erl:2499: :gen_server.handle_msg/3
    (stdlib 7.1) proc_lib.erl:333: :proc_lib.init_p_do_apply/3
Last message (from #PID<0.144.0>): :test
State: %{name: A, callee: B}
Client #PID<0.144.0> is alive

    (kernel 10.4.2) code_server.erl:163: :code_server.call/1
    (kernel 10.4.2) code.erl:587: :code.ensure_loaded/1
    (kernel 10.4.2) error_handler.erl:86: :error_handler.undefined_function/3
    (elixir 1.19.4) lib/exception.ex:753: Exception.format_mfa/3
    (elixir 1.19.4) lib/exception.ex:495: Exception.format_exit/2
    (elixir 1.19.4) lib/exception.ex:153: Exception.format_banner/3
    (iex 1.19.4) lib/iex/evaluator.ex:421: IEx.Evaluator.print_error/3
    (iex 1.19.4) lib/iex/evaluator.ex:327: IEx.Evaluator.safe_eval_and_inspect/3

16:24:34.211 [error] GenServer B terminating
** (stop) exited in: GenServer.call(A, :test, 5000)
    ** (EXIT) time out
    (elixir 1.19.4) lib/gen_server.ex:1142: GenServer.call/3
    (dead_lock 0.1.0) lib/dead_lock/worker.ex:24: DeadLock.Worker.handle_call/3
    (stdlib 7.1) gen_server.erl:2470: :gen_server.try_handle_call/4
    (stdlib 7.1) gen_server.erl:2499: :gen_server.handle_msg/3
    (stdlib 7.1) proc_lib.erl:333: :proc_lib.init_p_do_apply/3
Last message (from A): :test
State: %{name: B, callee: A}
Client A is alive

    (kernel 10.4.2) logger_config.erl:67: :logger_config.allow/2
    (stdlib 7.1) gen_server.erl:2785: :gen_server.error_info/7
    (stdlib 7.1) gen_server.erl:2744: :gen_server.terminate/9
    (stdlib 7.1) proc_lib.erl:333: :proc_lib.init_p_do_apply/3

すると、呼び出し元の iex プロセスには timeout が飛んできます。
すごく単純な例なので、何が起きているかは明白で、

  1. iex プロセスが DeadLock.Worker.test(A) を呼び出す
  2. A は handle_call で DeadLock.Worker.test(B) を呼び出す
  3. B は handle_call で DeadLock.Worker.test(A) を呼び出す

A は test(B) の処理をしている最中に B から test(A) を呼び出されるため、 GenServer の逐次処理の仕組みによって、B からの test(A) の処理を開始することができません。これによって、 timeout が起きます。※ GenServer.call/3 の デフォルトタイムアウトは 5 秒です。

「こんな書き方しないよ」、そう、おっしゃられると思います。私もそう思います。

ただ、 Appilcation 配下に複数の Supervisor をぶら下げ、それらを連携させて機能を発揮するようなコードを書く場合には注意が必要かもしれません。上の例のように childlen の関係が平たく書かれている場合にはすぐに気がつけますが、意図せず GenServer.call を複数サーバー間でループさせてしまったらどうでしょうか?このようなデッドロックは容易に起きえます。そして、このデッドロックはコンパイルによって検出することはできません。つまり、設計で防ぐしか無いのです。気をつけないといけませんね(自戒をこめて)

といったところで、複数 「GenServer でデッドロックを起こしてみる」でした。

以上です。

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?