Edited at

fish-shellによる並行処理の方法

More than 1 year has passed since last update.


適材適所

適材適所。その特性にふさわしい仕事につけること。

でも、適材適所とか言ってる余裕なんてない場合もある。それどころじゃない、とにかく fish-shell で並行処理ってどうやってできるのか!という時もあるよね。いや、本当に。

For example 自分が作った fish プラグインマネージャfishermanの場合、必ず fish 言語で書きたかったけど、パッケージのダウンロード・インストール・アップデートなど、そのへんはやっぱり並行処理が必要だった!

ちなみに、fishって全然知らない人は公式サイトにチュートリアルもあるからそれを見てみてね。

この記事では fish を使って、なんでもいいから並行処理のやり方を説明する!


コマンドをバックグラウンドで実行したい時


「&」を使う!

例えば


フォアグラウンド

sleep 1



バックグラウンド

sleep 1 &


の違いはなんだっけ?


  • 前者だと、シェルがsleep 1というコマンドの実行が終わるまでブロックしてる。要は、他の端末のウインドウかタブを開かないと、他には何もできなくて(他のコマンドを打ったりするとか)、待たないといけないってこと。しばらくプロセスをブロックしたい、という時は、sleep <seconds>がよく使われる。


  • 後者だとシェルがsleep 1コマンドを終わるのを待たないで、すぐ戻る。戻るというのはsleepコマンドがバックグラウンドで実行されるけど、シェルはいつも通りに、他のコマンドを打ったりするとかできる!


つまり、&が前につけるコマンドを async に実行してくれる。


「fish -c」もある

fish -c "sleep 5; echo 邪魔しにきたぞ" &

上のコマンドを打つと、何も変化がないまま Enter とかするたび改行が繰り返されるだけ。普段通りにシェルを使っていても平気。そして 5 秒たつといきなり邪魔しにきたぞってコマンドラインに表示される。

fish -c "..."は、node や ruby の-eオプションのように、引数からスクリプトファイル名を取らずに、"..."の部分を実行する方法だよ。


バックグラウンドタスクを待つには?


前述の方法

では、前の説明だけで並行処理をやってみよう。

set -l commands "sleep 1; echo "{1,2,3}

for cmd in $commands
fish -c "$cmd" &
end


順番が非決定性的なOutputになる

2

1
3

特に問題ないけど、もし$commandsの配列に入ってるコマンドたちを並行で実行したいのに、全部出てくるまで待たなきゃいけなかったら...:thinking:

ちょっと嫌だけど、fish ではwaitのようなシェルビルトインは今の所はないので、自分のを作らないと。


jobsコマンド

fish では、バックグラウンドに実行中タスクの ID や status を教えてくれるjobsというコマンドが使える。

fish -c "sleep 1; echo OK" &

jobs

Job Group   State   Command

1 95243 running fish -c "sleep 1; echo OK" &

jobsの output から ID を見て、どのバックグラウンドタスクが終わったか確認できる。でも、Commandの列をよく見るとタスクの全てのコマンドはそのまま表示されちゃうから、1 行しかないコマンドだったら特に問題ないけど、複数行の場合は困るね。


シンプルな例

fish -c "sleep 1

echo OK &"



不便なフォーマット

Job Group   State   Command

1 95310 running fish -c "sleep 1
echo OK" &

とにかく、jobsの output から ID を取り出すには、簡単な構文解析的な処理をしないと、並行処理のためにこのコマンドがうまく活かせない。


どこでも awk

じゃ、fish だけでこんな処理するのは面倒だから、どこでも awk でやっちゃう。


jobsからタスクのIDを取り出す

function get_jobs

jobs $argv | command awk -v FS=\t '
/[0-9]+\t/{
jobs[++nJobs] = $1
}
END {
for (i in jobs) {
print(jobs[i])
}
exit nJobs == 0
}
'

end

get_jobsの使い方はjobsと同じ。この場合に気になるのがタスクの ID。



  • get_jobs 全てのバックグラウンド実行中タスクの ID


  • get_jobs -l | --last 最後に実行された、まだ実行中バックグラウンドタスクの ID

というようになる。


実行する&待つ!DIY wait コマンド


  • 複数コマンドをバックグラウンドで実行できる

  • バックグラウンドで実行中のタスク(コマンドそれぞれのこと)の ID を取り出せる

  • バックグラウンドで実行中のタスク ID によってブロック(待つ)するには?:thinking:

タスク ID による、そのタスクが終わるまでにシェルをブロックさせるwait関数が望ましい。

function wait

while true
set
-l has_jobs
set -l all_jobs (get_jobs)
or break

for j in $argv
if contains -- $j $all_jobs
set -e has_jobs
break
end
end

if set -q has_jobs
break
end
end
end

この wait コマンドの使い方としては 2 つある。



  1. wait 実行中の全タスクが終わるまでブロック


  2. wait <ID> 教えたタスクが終わるまでブロック


実際にできることは?

ここまで読んでくれた人、どうもありがとう、お疲れ様!:tea: get_jobswaitが揃ったので、実際に使って何できるかを見てみましょう。

並行 vs 逐次の例として、たとえば、幾つかの url のダウンロード時間を比べたいとき。


concurrency_test.fish

function wait

while true
set
-l has_jobs
set -l all_jobs (get_jobs)
or break

for j in $argv
if contains -- $j $all_jobs
set -e has_jobs
break
end
end

if set -q has_jobs
break
end
end
end

function get_jobs
jobs $argv | command awk -v FS=\t '
/[0-9]+\t/{
jobs[++nJobs] = $1
}
END {
for (i in jobs) {
print(jobs[i])
}
exit nJobs == 0
}
'

end

set -l urls "https://"{google,twitter,youtube,facebook,github,qiita}".com"

for url in $urls
fish -c "curl -Lw \"$url: %{time_total}s\n\" -o /dev/null -s $url" &
end

wait (get_jobs)


さー、いよいよ実行して、結果を見てみよう。


並行処理結果

fish < concurrency_test.fish

https://qiita.com: 1.205s
https://google.com: 1.386s
https://github.com: 2.022s
https://facebook.com: 2.169s
https://twitter.com: 2.256s
https://youtube.com: 2.796s
~ 2s 836ms

ちなみに、並行処理のところを抜けて、逐次処理で実行した結果は


serial_test.fish

set -l urls "https://"{google,twitter,youtube,facebook,github,qiita}".com"

for url in $urls
curl -Lw "$url: %{time_total}s\n" -o /dev/null -s $url
end



逐次処理

fish < serial_test.fish

https://google.com: 1.655s
https://twitter.com: 1.444s
https://youtube.com: 2.250s
https://facebook.com: 1.292s
https://github.com: 1.351s
https://qiita.com: 0.184s
~ 8s 225ms

やっぱり並行処理は楽しいね!