yu-tarrrrの日記

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

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のパターンマッチは非常に協力だなとますます感じた。