LoginSignup
8
2

More than 5 years have passed since last update.

並列実行 ElixirのTaskとFlow/GenStage、bashとGNU parallel

Last updated at Posted at 2018-04-24

Qiita初投稿のCayenneRyoです。
なんか間違ったこともしちゃうかもしれませんが、モヒカン族からの襲撃を歓迎します。

さて、先日fukuoka.ex#8(福岡Elixir会):春のElixir入学式に参加しました。
その中のzackyさんプレゼンを聴いて、なんか似たようなことがあったな、と思ったことを書いてみます。

今時、各言語で、並列実行の方法が準備されてるけど、うまく使わないと実行時間が早くならないよ、と言う話。
例えば、単に5秒スリープするだけの処理を10本実行する場合を考える。
普通に考えれば、ほぼ何もしない処理なので、同時並列に実行されて、5秒ちょっとで終わるのではないか、とかんがえるところだが…

Elixirの並列実行: TaskとFlow/GenStage

Elixir歴二週間未満なので、zackyさんのプレゼンから丸パク。

Elixir: Task

Elixirに標準で入っているTaskを使った並列実行。
:timer.tcを使うと、マイクロ秒で実行時間を計測できる。

iex(1)> :timer.tc(fn ->
...(1)> 1..10
...(1)> |> Enum.map(fn i ->
...(1)>      Task.async(fn -> Process.sleep(5000); i end)
...(1)>    end)
...(1)> |> Enum.map(&Task.await &1)
...(1)> end)
{5011513, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}
iex(2)>

すばらしい! 予想通り5秒ちょっとで実行できた!
※なぜか手元の環境では、たまにエラーになることがあるが、理由は不明。その内調べる…

Elixir: Flow/GenStage

何も考えなくてそれなりに巧く並列実行してくれるFlow
mixでプロジェクトを作って、mix.exsの中のdefpFlowを加えて…
とゴニョって、iex -S mixを実行。

iex(1)> :timer.tc(fn ->
...(1)> 1..10
...(1)> |> Flow.from_enumerable
...(1)> |> Flow.map(fn i -> Process.sleep(5000); i end)
...(1)> |> Flow.partition
...(1)> |> Enum.to_list
...(1)> end)
{50052699, [3, 4, 2, 6, 8, 10, 1, 5, 7, 9]}
iex(2)> 

およ?! これって50秒かかっているのでは?
前述のzackyさんのプレゼンによれば、Flowは処理を適当な数の塊にしてElixirのプロセスに渡すし、並列度も調節してくれるのだが、今回の様に中身がスリープのみと言うようなケースでは、それが仇になってしまうのだとか。

それを避けるには、Flow.from_enumerableに適切なオプションを指定すればいいらしい。

iex(2)> :timer.tc(fn ->
...(2)> 1..10
...(2)> |> Flow.from_enumerable(max_demand: 1, stages: 10)
...(2)> |> Flow.map(fn i -> Process.sleep(5000); i end)
...(2)> |> Flow.partition
...(2)> |> Enum.to_list
...(2)> end)
{5003564, [2, 1, 6, 5, 8, 9, 3, 4, 7, 10]}
iex(3)>

素晴らしい! 5秒台!!
この例では、同時に実行する:stagesを10、バッチサイズ:max_demandを1にして、すべての処理が同時並列で実行されるよう調整している。

と、ここまでがzackyさんのプレゼンのパクリ。

Bashの並列実行

で、同じことをシェルスクリプト・bashでやるとどうなるか?

Bash: 通常のバックグラウンド実行とwait

ご存知の通り、シェルスクリプトは、コマンドの末尾に「&」を付けるとバックグラウンドで実行してくれる。
バックグラウンドジョブの終了を待つには、waitコマンドを使う。

$ time (for NUM in $(seq 1 10); do (sleep 5; echo $NUM)& done; wait $(jobs -p))
1
2
4
5
3
7
8
6
9
10

real    0m5.071s
user    0m0.043s
sys     0m0.114s
$ 

ゐぇい! 5秒台。
ちなみに、Elixirに影響されて、配管しようとしてはいけない。

\$ time (seq 1 10 | while read NUM; do (sleep 5; echo \$NUM)& done; wait $(jobs -p))

とやっても上手く行かない(実装が多い)。
これは、jobs -pが空を返すから。
パイプ「|」の後は、サブシェルで実行される。
サブシェルの中で起動されたバックグラウンドジョブは、親ジョブからは見えない。

Bash: GNU Parallelで並列実行

GNU Parallelは、並列実行を巧いことやってくれるツール。
しかし、使い方をよく考えないと、ElixirのFlowと同じようなことが起こる。

$ time (seq 1 10 | parallel "sleep 5; echo {}")
Academic tradition requires you to cite works you base your article on.
When using programs that use GNU Parallel to process data for publication
please cite:

  O. Tange (2011): GNU Parallel - The Command-Line Power Tool,
  ;login: The USENIX Magazine, February 2011:42-47.

This helps funding further development; AND IT WON'T COST YOU A CENT.
If you pay 10000 EUR you should feel free to use GNU Parallel without citing.

To silence the citation notice: run 'parallel --bibtex'.

1
2
3
4
5
6
7
8
9
10

real    0m25.623s
user    0m0.361s
sys     0m0.551s
$

実際に実行される様子を見てみると判るが、同時には二つづつしか実行されない。
これは、GNU Parallelがコア数などを見て調整した結果。

これを避けるには、--jobs Nオプションを使う。

$ time (seq 1 10 | parallel --jobs 10 "sleep 5; echo {}")
Academic tradition requires you to cite works you base your article on.
When using programs that use GNU Parallel to process data for publication
please cite:

  O. Tange (2011): GNU Parallel - The Command-Line Power Tool,
  ;login: The USENIX Magazine, February 2011:42-47.

This helps funding further development; AND IT WON'T COST YOU A CENT.
If you pay 10000 EUR you should feel free to use GNU Parallel without citing.

To silence the citation notice: run 'parallel --bibtex'.

1
2
3
4
5
6
7
8
9
10

real    0m5.746s
user    0m0.354s
sys     0m0.661s
$

よし、5秒台。

考察

今回の例は、単に5秒間スリープすると言う、極端にCPU資源を消費しない特殊な処理だった。
なので、こんな検証に意味はない…わけでもない。
たとえば、ウェブAPIの応答待ちが処理時間の大半を占めるというようなものだと、似たような状況になる。

ElixirのFlowやGNU Parallelは、並列処理の実装に便利なツールだが、上の様な処理の場合には、期待通りには動かないことを知っておくべきだろう。

次は、PowerShellの場合についても検証してみたい。

8
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2