Elixirでいい感じに並行処理を行う
はじめに
- 今回は表題の通りです
- よくあるケースでAPIコールを並列で行いたい時に、Elixirだったらどうするのかというのを残しておきたいと思います
実践
- まず、公式を参照する
Task.async/ 1
という関数で非同期的に処理を呼び出せて、Task.await /2
という関数でその処理の終了を待つというもの- 至ってシンプルで、他の言語でもあるようなasync/awaitの組み合わせです
- ということで実際に動かしてみる
別関数を呼び出して、printするだけというもの。 Task.async()の引数はfunctionを求められるので、そこだけ要注意。 あとは、awaitしてあげれば良いだけという。
defmodule Sample do def hello(arg) do task = Task.async(fn -> print_str(arg) end) Task.await(task) end def print_str(str) do IO.puts(str) str end end
テストも特に特殊なことはしなくて良い。
defmodule SampleTest do use ExUnit.Case test "call hello" do assert Sample.hello("hoge") == "hoge" end end
- 実際に並列処理を行う場合、単一の呼び出しは考えられないので、複数の呼び出しをしてみる
defmodule Sample do def hello(arg) do task = Task.async(fn -> print_str(arg) end) task2 = Task.async(fn -> print_num(1) end) Task.await(task2) Task.await(task) end def print_str(str) do IO.puts(str) str end def print_num(num) do IO.puts(num) num end end
処理自体に意味はなく、複数の呼び出しをしてみた。 先述のテストを通すために、awaitをかける順番に意図を持たせてしまったが、基本的に呼び出しが増えてもやるべきことは一緒。
- エラーハンドリングを追加していく
- まずはテストケースから
2つの引数を渡して、数字は偶数、文字列は7文字より小さければ成功する処理を書く
defmodule SampleTest do use ExUnit.Case test "call hello success" do assert Sample.hello("hogeho", 2) == {:ok, "hoge", 2} end test "call hello fail of string length is longer than 6" do assert Sample.hello("hogehog", 2) == :error end test "call hello fail of number is not even" do assert Sample.hello("hoge", 1) == :error end end
defmodule Sample do def hello(arg1, arg2) do task = Task.async(fn -> print_str(arg1) end) task2 = Task.async(fn -> print_even(arg2) end) handle(Task.await(task), Task.await(task2)) end defp handle({:ok, result}, {:ok, result2}), do: {:ok, result, result2} defp handle(_, _), do: :error def print_str(str) do if(str |> String.length() < 7) do {:ok, str} else :error end end def print_even(num) do if num |> rem(2) == 0 do {:ok, num} else :error end end end
上記のような感じになる。 handlingする用に同一名で引数違いのfunctionを定義しておいて、そこでパターンマッチさせる。 そうすることで、割と楽に例外処理を行うことができる。
- では、3件以上の呼び出しをどうするか。 現状は2回の呼び出しを行うだけなのでTask.async()を呼び出すのも2回で済むが、これが比例して増えるのはアプリケーションコードとしてはあまりイケてないのでそこら辺をいい感じにしていく
defmodule SampleTest do use ExUnit.Case test "call hello success" do assert Sample.hello("hoge", 2) == {:ok, {"hoge", 2}} end test "call hello fail of str" do assert Sample.hello("hogehoge", 2) == :error end test "call hello fail of num" do assert Sample.hello("hoge", 1) == :error end end
そのために、まずはテストの修正。 正常系のレスポンスのタプルの形が扱いにくかったので修正をしておく。
defmodule Sample do def hello(arg1, arg2) do [fn -> print_str(arg1) end, fn -> print_even(arg2) end] |> Enum.map(&Task.async(&1)) |> Enum.reduce_while({:ok, {}}, fn task, {:ok, response} -> case Task.await(task) do {:ok, res} -> {:cont, {:ok, Tuple.append(response, res)}} :error -> {:halt, :error} end end) end def print_str(str) do if(str |> String.length() < 7) do {:ok, str} else :error end end def print_even(num) do if num |> rem(2) == 0 do {:ok, num} else :error end end end
そして、実装の修正。 呼び出しのところを、ListにいれてあとはMapで処理を進める。そして、reduceでくっつけてしまうというだけの処理。 思ったよりシンプルになった。
まとめ
- 今回は簡単に並列処理を段階的に書き換えて行って、少しは良い書き方ができたのではないかと思う。
- あと、Elixirのパターンマッチは非常に協力だなとますます感じた。