これは Elixir Advent Calendar 2025 の 23日目です。
Elixir/Erlang のプロセスでいろいろやってて、ちょいとプロセスの実行を止めたくなりました。そしたら沼とまでは言いませんが水溜りに足突っ込んじゃったみたいになったので共有します。なお、ここで言う「プロセスを止める」は再開可能な状態にする停止で「実行終了」の意味ではないことに注意してください。
自分で自分を止めようとしたら
プロセス操作は Erlang の sys モジュール関数で suspend/2 と resume/2 があります12。第1引数が pid で第2引数がタイムアウト(省略時 5000ms)です。自分自身を指定して止まろうとしてみました。
iex(1)> :sys.suspend(self())
** (exit) exited in: :sys.suspend(#PID<0.165.0>)
** (EXIT) process attempted to call itself
(stdlib 7.1) sys.erl:755: :sys.send_system_msg/2
iex:1: (file)
なあんと、自分じゃ自分を止められません。いいじゃん、本人が止まりたいって言ってるんだから。
ではというので Elixir の Process モジュールの関数 sleep/1 を使ってみます3。これの引数はお休みしている時間 (ms) です。勝手に起きないように :infinity で眠ることにします。
iex(1)> Process.sleep(:infinity)
とすると確かに眠りにつきます。が、iex 自体が止まってるので起こす方法がありません。iex のプロセス自体で実験してるといろいろ不都合です。
iex から一旦プロセスを spawn して操作する
iex で自分を止めるのはどうもうまく行かなさそうなので、別のプロセスを一旦経由して自分を止めてみます。
深く考えずに spawn した先でやってもらおうとしました。
iex(6)> Process.spawn(:sys.suspend(self()), [:monitor])
** (exit) exited in: :sys.suspend(#PID<0.115.0>)
** (EXIT) process attempted to call itself
(stdlib 7.1) sys.erl:755: :sys.send_system_msg/2
iex:5: (file)
iex(7)> self()
#PID<0.115.0>
ん〜、これだと先に :sys.suspend を先に評価してしまうから駄目ですね。じゃあってんでこれはどうだ。
iex(22)> self()
#PID<0.115.0>
iex(23)> Process.spawn(:sys, :suspend, [self()], [:monitor])
{#PID<0.129.0>, #Reference<0.511196337.4289986561.27534>}
こんどは例外こそ起こらないけど、iex 自体が止まったりしません。ここで self() がなにを返しているかというと
iex(25)> self()
#PID<0.115.0>
iex(26)> Process.spawn(:sys, :suspend, [IO.inspect(self())], [:monitor])
#PID<0.115.0>
{#PID<0.131.0>, #Reference<0.511196337.4289986561.27620>}
と思ったとおりプロセスIDが渡っています。spawn した先の self() の結果ではないですね。もう少し詳しく見てみます。
iex(37)> self()
#PID<0.115.0>
iex(38)> Process.spawn(:sys, :suspend, [self()], [:monitor])
{#PID<0.134.0>, #Reference<0.511196337.4289986561.28062>}
iex(39)> flush # すぐに実行
{:system,
{#PID<0.134.0>, [:alias | #Reference<0.0.17155.511196337.4290052097.28064>]},
:suspend}
:ok
iex(40)> flush # ちょっと待って実行
{:DOWN, #Reference<0.511196337.4289986561.28062>, :process, #PID<0.134.0>,
{:timeout, {:sys, :suspend, [#PID<0.115.0>]}}}
:ok
あれえ、なんだか子プロセスは親プロセスを :suspend しようとしてできなくて :timeout しているっぽいですね。IEX が :sys.suspend を受け付けないのかもです。
Spawn した先のプロセスを止める
どうも iex 自身になにかしようとすると何かとよろしくなさそうなのでやり方を変えます。今度は逆に iex で spawn してできたプロセスに対してやってみます。
iex(5)> flush
:ok
iex(6)> self()
#PID<0.115.0>
iex(7)> {pid, ref} = Process.spawn(fn -> :sys.suspend(self()) end, [:monitor])
{#PID<0.117.0>, #Reference<0.781389651.1347420164.131218>}
iex(8)> flush
{:DOWN, #Reference<0.781389651.1347420164.131218>, :process, #PID<0.117.0>,
{:calling_self, {:sys, :suspend, [#PID<0.117.0>]}}}
:ok
お、いい感じに停止したプロセスが作れたような。こいつを起こしてみましょう。
iex(9)> :sys.resume(pid)
** (exit) exited in: :sys.resume(#PID<0.117.0>)
** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't started
(stdlib 7.1) sys.erl:755: :sys.send_system_msg/2
iex:9: (file)
iex(9)> flush
:ok
えええ!? プロセスを suspend して resume したらプロセスが起きてくるんじゃないの? いきなり寝た状態のプロセスとか作るからおかしなことになるのかなぁ。Spawn で作ったプロセスはダメなんてことある? ちょっとこれは理由がわかりませんでした。
GenServer でやってみる
やっぱりプロセスを扱うなら手慣れた技を使うのが良いですね。今度は GenServer でプロセス作って、それを寝かせたり起こしたりできるかみてみます4。
簡単なプログラムを書いてみます。プロセスにメッセージを送ると hello world! と返事するようにしてあります。
defmodule GenTest do
use GenServer
def start_link(pname) do
GenServer.start_link(__MODULE__, [], name: pname)
end
def init(state) do
{:ok, state}
end
def handle_info(_, state) do
IO.puts("hello world!")
{:noreply, state}
end
def suspend(pname) do
:sys.suspend(pname)
end
def resume(pname) do
:sys.resume(pname)
end
end
これでどうなるか試してみます。まずは iex を起動します。
iex -S mix
Erlang/OTP 28 [erts-16.1.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
Compiling 1 file (.ex)
Generated gentest app
Interactive Elixir (1.19.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
GenServer のプロセスを :afo という名前でつくります。それに順に
- メッセージを送る
- suspend する
- メッセージを送る
- resume する
をやってみます。
iex(1)> GenTest.start_link(:afo)
{:ok, #PID<0.161.0>}
iex(2)> send(:afo, "hello")
hello world!
"hello"
iex(3)> GenTest.suspend(:afo)
:ok
iex(4)> send(:afo, "hello")
"hello"
iex(5)> GenTest.resume(:afo)
hello world!
:ok
iex(6)>
立ち上げてすぐにメッセージを送ると hello world! が返ってきます。その後 suspend で眠らせた後にメッセージを送っても返事が返ってきません。これはプロセスが止まってるのでメッセージが処理されずにプロセスのメッセージキューに溜まっているからです。resume でプロセスを起こしたらキューに溜まってるメッセージが処理されて返事を返してきました。
やっぱり GenServer のプロセスはわかりやすいですね。ちゃんと想定通りの動作をしてくれました。
ついでに気づいたこと
今回の話と直接関係ないのですがきづいたことをメモっておきます。
Process.send_after のタイムアウト
なんかうまいこと使える技がないか探しているときに気がついたことで、
iex(1)> Process.send_after(self(), :wakeup, :infinity)
とか書けません。第3引数の待ち時間は整数であって :infinity を書くことができません。まあ、当たり前なんですが。
GenServer の :hibernate オプション
組込みやってる方なら sleep とか hibernation と聞くと「お、チップが省電力モードに入るのかしら」とか思いますよね。GenServer の start_link/3 はこんなこと書けたりします。
GenServer.start_link(__MODULE__, state, [hibernate_after: :infinity])
あとコールバック関数の返り値として {:reply, reply, new_state, :hibernate} などと書くことができます。ひょっとしたら組込みやIoTで使えるなにかかと期待が高まります。
Elixir GenServer での説明としては handle_call/3 が一番詳しくて「処理すべきメッセージがなければ強制的にガーベジコレクションする」旨が書いてあります5。
「え、それだけ?」と拍子抜けしてしまいます。
これ Erlang の proc_lib や erts のドキュメントにはもっと詳しく書いてあります67。プロセスのスタックとヒープをきれいにしておくようです。大量のプロセスがあるけど、寝たまま静かにしているプロセスがかなりの割合になる場合に全体としての資源消費を適正にするためと理解しました。組込みへの応用で消費電力を落とすための機能というのではなさそうです。