ヽ(´・肉・`)ノログ

How do we fighting without fighting?

すごいE本をElixirでやる(25)

第 11 章マルチプロセスについてもっと - 11.3 タイムアウト から

11.3 タイムアウト

Erlang の pid(0,255,0)IEx.pid/3 で行える. IEx にはあらかじめ import されているので,モジュール名をつけて IEx.pid(0,255,0) としなくても関数だけで pid(0,255,0) と利用できる.

defmodule Kitchen do
  def fridge1 do
    receive do
      {from, {:store, _food}} ->
        send from, {self, :ok}
        fridge1
      {from, {:take, _food}} ->
        # uh...
        send from, {self, :not_found}
      :terminate ->
        :ok
    end
  end

  def fridge2(food_list) do
    receive do
      {from, {:store, food}} ->
        send from, {self, :ok}
        fridge2([food | food_list])
      {from, {:take, food}} ->
        if Enum.member?(food_list, food) do
          send from, {self, {:ok, food}}
          # List.delete/2 doesn't match to use here.
          # `List.delete([1, 2, 2, 3], 2)` returns `[1, 3]`
          # But we need `[1, 2, 3]`
          fridge2(:lists.delete(food, food_list))
        else
          send from, {self, :not_found}
          fridge2(food_list)
        end
      :terminate ->
        :ok
    end
  end

  def store(pid, food) do
    send pid, {self, {:store, food}}
    receive do
      {pid, msg} -> msg
    end
  end

  def store2(pid, food) do
    send pid, {self, {:store, food}}
    receive do
      {pid, msg} -> msg
    after 3000 ->
      :timeout
    end
  end

  def take(pid, food) do
    send pid, {self, {:take, food}}
    receive do
      {pid, msg} -> msg
    end
  end

  def take2(pid, food) do
    send pid, {self, {:take, food}}
    receive do
      {pid, msg} -> msg
    after 3000 ->
      :timeout
    end
  end

  def start(food_list) do
    spawn(__MODULE__, :fridge2, [food_list])
  end
end
Eshell V8.0.2  (abort with ^G)
Interactive Elixir (1.3.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> c("kitchen.exs")
c("kitchen.exs")
warning: variable pid is unused
  kitchen.exs:39

warning: variable pid is unused
  kitchen.exs:46

[Kitchen]
iex(2)> Kitchen.take(pid(0,250,0), :dog)
Kitchen.take(pid(0,250,0), :dog)

確かにシェルが固まってしまう.

一般的に、非同期処理(Erlang のメッセージパッシングもそうです)を行うときは、一定時間を過ぎてもデータを取得できる気配がないときに諦めるための手段が必要

覚えておこう.非同期を考えるときは,一定時間過ぎたときの諦める方法も併せて考える.

Elixir でも Erlang と同じタイムアウト方法をとれる. Kernel.SpecialForms.receive/1 を参照のこと.以下のような記述になる.

receive do
  (マッチ) -> (式1)
after (時間) ->
  (式2)
end

冒頭にあげたコードの store2take2 で利用されているのがわかるだろうか. 試してみると,意図したとおり3秒後に :timeout が表示されて,シェルへと制御が戻ってくる.

iex(2)> c("kitchen.exs")
c("kitchen.exs")
warning: redefining module Kitchen (current version loaded from Elixir.Kitchen.beam)
  kitchen.exs:1

warning: variable pid is unused
  kitchen.exs:39

warning: variable pid is unused
  kitchen.exs:46

warning: variable pid is unused
  kitchen.exs:55

warning: variable pid is unused
  kitchen.exs:62

[Kitchen]
iex(3)> Kitchen.take2(pid(0,250,0), :dog)
Kitchen.take2(pid(0,250,0), :dog)
:timeout
iex(4)>

たとえば timer:sleep/1 の実装で使われています

Erlang で一定時間 sleep させたいときに :timer.sleep/1receive のタイムアウトを利用する実装の両方を見たことがある (Elixir だとさらに Process.sleep/1 もある……)

どれが望ましいのかわからなかったが,結局は同じことをしているのか.

Process.sleep/1 の実装は receive を使ったものになっているな.今後はこれでいこう.