Elixirで関数の非同期呼び出しを行って結果に対して逐次的に別の処理をかける

はじめに

Elixirで非同期をやる場合Task.asyncをつかいますが、非同期で呼び出した複数の関数の呼び出し結果に対して、逐次的に別の処理をかけたい状況で、同僚のElixirに詳しい人のコード見てなるほどと思ったのでメモとして残しておきます。

どうするか?

具体的に下記の手順を踏みます。

  1. 関数のリストを作成する
  2. 作成したリストにEnum.mapTask.asyncを呼びだす
  3. Enum.each等でasyncされたタスクのリストに対してTask.awaitを呼び出して結果に対して処理を行なう

実装してみる

まずは非同期で呼び出される関数の方を実装します。

defmodule Chores.FirstTask do
  def do_something do
    :timer.sleep(3000)
    IO.puts "finised first task after wating 3s"
    "sleeped 3s"
  end
end


defmodule Chores.SecondTask do
  def do_something do
    :timer.sleep(2000)
    IO.puts "finised second task after wating 2s"
    "sleeped 2s"
  end
end

Chores.FirstTask.do_somethihg/0は最初に呼び出されることを想定した関数です。開始時に3秒待ち、"sleeped 3s"の文字列を返します。
Chores.SecondTask.do_somethihg/0は二番目に呼び出されることを想定した関数で、Firstと違って2秒待ってから"sleeped 2s" の文字列を返します。
次はこれらの関数を非同期で呼び出すところを実装します。

defmodule AsyncExample do
  alias Chores.{FirstTask, SecondTask}

  def do_tasks do
    [
      fn -> FirstTask.do_something end,
      fn -> SecondTask.do_something end,
    ]
    |> Enum.map(&Task.async(&1))
    |> Enum.each(fn t ->
      Task.await(t)
      |> IO.puts
    end)
  end
end

このAsyncExample.do_task/0は基本的には前述の手順通りです。
最後のIO.putsでそれぞれの関数の返り値を標準出力に出力しています。

実行する

async_example.exsに先程のモジュールを定義して実行してみます。

$ time elixir async_example.exs 
finised second task after wating 2s
finised first task after wating 3s
sleeped 3s
sleeped 2s

real    0m3.260s
user    0m0.573s
sys 0m0.066s

finised second task after wating 2sが先に出力されていることと、実行時間が約3秒であることからタスクが非同期に実行されていることがわかります。
その後、それぞれの関数が返すsleeped 3ssleeped 2sが出力されます。ここで遅れて実行が完了するsleeped 3sのほうが先に出力されるのはEnum.eachの処理順はあくまで関数のリストの順番だからだと思います。

結果をエラーハンドリングしたい場合

実行したタスクのエラーハンドリングを行いたい場合はEnum.eachの代わりにEnum.while_reduceを使って行なうことができます。Enum.while_reduceを用いたエラーハンドリングについては前にブログを書いたのでそちらを良ければ見てください。

利用シーン

例えば、HTTPリクエストでJsonを取得してそのJsonをマージするなどと行った場合に使えるかなと思います。