はじめに
- 今回は前回の応用編の続き
- 並行プログラミングについて学んでいく
並行性
actors
- Elixirでは
actor model
と呼ばれる並行プログラミングの手法が使用できる 参考
- このモデルでは
actor
たちは並行で動く独立した entities
である
actor
は独立しているので、他の actor
に対して状態を共有しないので、競合状態になることはない
actor
同士はお互いにメッセージと呼ばれるものを送ることでコミュニケーションしていて、一つの actor
はメッセージを順次処理していく
- 並行性というのは、いくつかの
acotor
が並行に実行されることによって生じるものである
process
actor
は process
とも呼ばれる
- 多くの言語では、プロセスは重いものである。一方で、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)
- こっちの方は
async
とawait
を呼び出すという他の言語でも馴染みのある関数
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の並行プログラミングの触りのところを理解した