yu-tarrrrの日記

完全に個人の趣味でその時々書きたいことを書く

Elixir School 4日目

はじめに

  • 今回は前回の応用編の続き
  • 並行プログラミングについて学んでいく

並行性

actors

  • Elixirでは actor modelと呼ばれる並行プログラミングの手法が使用できる 参考
  • このモデルでは actorたちは並行で動く独立した entitiesである
  • actorは独立しているので、他の actorに対して状態を共有しないので、競合状態になることはない
  • actor同士はお互いにメッセージと呼ばれるものを送ることでコミュニケーションしていて、一つの actorはメッセージを順次処理していく
  • 並行性というのは、いくつかの acotorが並行に実行されることによって生じるものである

process

  • actorprocessとも呼ばれる
  • 多くの言語では、プロセスは重いものである。一方で、Elixirではリソース消費と起動速度という観点からスレッドよりも軽量である
  • そのため、Elixirでは幾千ものプロセスが走ることがある(Elixirではスレッドプールみたいなものを用意して、管理する必要はない。そこらへんは、Erlang VMが頑張ってくれる)
  • message passingによってElixirの並行プログラミングは動く

Messages and Mailboxes

  • Messagesは非同期である
  • actorにMessageを投げると、actorのMailboxに置かれる
  • actorの呼び出しはノンブロッキングに実行される
  • MailboxはElixirにおいては queueである
  • actorが準備ができていれば、MailboxからMessageを引き出し、Messageに応答し他のMessageを送る。それを、Mailboxが空になるまでactorは一連の流れを繰り返す

spawn and spawn_link

  • 新しいプロセスを作る最も簡単な方法は、匿名/名前付き関数を引数に取る spawn。 新しいプロセスが作られると、 プロセス識別子 別名PIDが返る。これはアプリケーション内部でプロセスを一意に識別するものである。
one_message = fn () ->
  receive do
    {:hello} -> IO.puts(“HI!”)
  end
end
actor = spawn(one_message) 
  • 上記の例だと、一度recieveの部分で期待してるメッセージがきた際に、actorのライフサイクル的には終了する
  • なので、次は1度きりではなく、期待するメッセージが来てもなんども処理できるようなプログラムにする
defmodule HiThere do
  # recieve block
  def hello do
    receive do
      {:hello} ->IO.puts(“HI!")
    end
 # infinite loop
  hello
  end
end
  • いくつか走っているプロセスのうち1つが終了した場合(正常終了orクラッシュした場合を問わず)、他のプロセスに知らせてあげる必要がある
  • その場合に、spawn_linkを使う
defmodule Example do
  def explode, do: exit(:kaboom)

  def run do
    Process.flag(:trap_exit, true)
    spawn_link(Example, :explode, [])

    receive do
      {:EXIT, _from_pid, reason} -> IO.puts("Exit reason: #{reason}")
    end
  end
end
  • spawn_linkが呼ばれると、リンクしたプロセスがお互いに通知を受け取るので、受け取った側のプロセスも死ぬ。
  • iexでも、spawn_linkを呼び出すと、プロセスが両方死んだことがわかる
iex(11)> spawn(Example, :explode, [])
#PID<0.147.0>

iex(12)> spawn_link(Example, :explode, [])
** (EXIT from #PID<0.105.0>) shell process exited with reason: :kaboom

Interactive Elixir (1.10.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
  • リンクしたプロセスは殺さずに、 spawn_linkを呼び出したプロセスだけを殺すためには、Process.flag/2を使用してあげる
  • Process.flag/2の第1引数には :trap_exitをというフラグを渡してあげて、第2引数にはtrue/falseを渡してあげる(この場合はフラグを立ててあげる必要がある)
defmodule Example do
  def explode, do: exit(:kaboom)

  def run do
    Process.flag(:trap_exit, true)
    spawn_link(Example, :explode, [])

    receive do
      {:EXIT, _from_pid, reason} -> IO.puts("Exit reason: #{reason}")
    end
  end
end

Task

  • Taskは関数をバックグラウンドで実行し、後でその戻り値を受け取る
  • アプリケーションの動作を妨げることなく、実行コストの高い演算を処理する時に特に役立てることができる
defmodule Example do
  def double(x) do
    :timer.sleep(2000)
    x * 2
  end
end

iex> task = Task.async(Example, :double, [2000])
iex> Task.await(task)
  • こっちの方はasyncawaitを呼び出すという他の言語でも馴染みのある関数

genserver

  • genserverはclient-serverという関係のサーバーを実装するためのビヘイビアモジュールある
  • genserverはビヘイビアの一種なので、実装する上で呼び出す必要がある関数がいくつかある(同期・非同期で異なる)
defmodule SimpleQueue do
  use GenServer

  # クライアント側
  def start_link(state \\ []) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  # コールバック関数
  def init(state), do: {:ok, state}
end
  • あとは、同期関数のcallを呼び出すか非同期関数のcastをよびだすかによって、コールバック関数の定義も異なる
  def print(n) do
    GenServer.call(__MODULE__, {:print, n})
  end

  def print_async(n) do
    GenServer.cast(__MODULE__, {:print, n})
  end

  def handle_call({:print, n}, _from, state) do
    {:ok, fb, state} = sum(n, state)
    {:reply, fb, state}
  end

  def handle_cast({:print, n}, state) do
    {:ok, fb, state} = sum(n, state)
    IO.puts fb
    {:noreply, state}
  end

  def sum(n, state) do
    ...
  end

まとめ

  • 今回はElixirの並行プログラミングの触りのところを理解した