GNU makeは最強の並列スクリプト言語。ビルドだけではもったいない
並列処理の複雑さ
例えば以下の様なスクリプトを組むとします。
- WebサーバーAにアクセスするとレスポンスとして4つのURLが得られる。それらは下記の売り上げ数量サーバー4台(B,C,D,F)のURL。
- 売り上げ数量サーバー(B,C,D,F)のレスポンスはそれぞれが対応する4店の売り上げ数量。
- WebサーバーGのレスポンスは商品単価。
- スクリプトでは売り上げ数量サーバーから商品売り上げ数量を取得し、WebサーバーGの商品単価を掛け合わせた合計を表示する。
単純にbashとcurlでスクリプトを書くと以下の様になるでしょう。
curl http://[WebサーバーA] > URLs #1
売り上げB=`curl $(sed -n '1p' URLs)` #2
売り上げC=`curl $(sed -n '2p' URLs)` #3
売り上げD=`curl $(sed -n '3p' URLs)` #4
売り上げE=`curl $(sed -n '4p' URLs)` #5
単価=`curl http://[WebサーバーG] ` #6
echo $(売り上げB) $(売り上げC) $(売り上げD) $(売り上げE) $(単価) |awk '{print ($1+$2+$3+$4)*$5}' #7
処理は当然、上から下に実行されます。#1->#2->#3->#4->#5->#6->#7。
しかし、これはあるサーバーのレスポンスが来ないと次のサーバーにアクセスしないのでとても不効率です。
bashではアンバサンド&でバックグラウンドで動作させることもできます。しかし、これでは結果を揃える待ち合わせをしなければいけなくなるのでとても煩雑になってしまいます。
理想的には以下の順番で並列に実行するのが一番効率がよさそうです。
#1と#6を並列実行
#1が終了次第、#2から#5を実行
#1から#6が終了後#7を実行
これは例え複数タスクをサポートしているプログラミング言語でもちょっと面倒そうな処理です。
GNU makeをスクリプト言語として使う
ここでGNU makeの登場です。コードは以下の通り。(わかりやすくする為、自動変数は使っていません)
all:売り上げB 売り上げC 売り上げD 売り上げE 単価
echo $(shell cat 売り上げB) $(shell cat 売り上げC) $(shell cat 売り上げD) $(shell cat 売り上げE) $(shell cat 単価) |awk '{print ($$1+$$2+$$3+$$4)*$$5}'
URLs:
curl http://[WebサーバーA] > URLs
売り上げB: URLs
curl $(shell sed -n '1p' URLs) >売り上げB
売り上げC: URLs
curl $(shell sed -n '2p' URLs) >売り上げC
売り上げD: URLs
curl $(shell sed -n '3p' URLs) >売り上げD
売り上げE: URLs
curl $(shell sed -n '4p' URLs) >売り上げE
単価:
curl http://[WebサーバーG] > 単価
なんとこれだけです。依存関係の記述やbashとmakeの書き方の差異(実行結果を使うshell catや$のエスケープ)で多少長くなっている以外はほとんどbashの時と変わりません。
実行(make)してみると、ちゃんと期待通りの順番で実行されます。しかし、相変わらずcurlリクエストは前のレスポンスが来るのを待ってから次のリクエストが発行されている様です。
bashの時と変わらないのでしょうか、いいえ、GNU makeには複雑な並列処理をたった2文字で実現してしまう魔法があるのです。
make -j
それが-jオプションです。これは「出来るだけ並列に実行する」というオプションです。これがGNU makeに実装されているjobserverという非常に強力な並列処理機能です。
むやみに並列にする訳ではなく、ちゃんと依存関係を見て、待ち合わせの必要な場合はその通りに待ち合わせてくれます。
上記の例だと
- URLsと単価を同時リクエスト
- URLsが取得次第、売り上げサーバー4台へ並列リクエスト
- 売り上げ4つと単価が揃ったら結果を計算表示
といった具合です。
Makefileをスクリプトとして仕上げる
bashにはshebangという機能があります。これはbash以外の言語で書かれたスクリプトを認識して適切なインタープリタを実行するものです。
これはGNU makeにも使えます。
#!/usr/bin/make -f
all:
echo Hello World
これでMakefileという名前ではなく例えば「hello」という名前でもスクリプトとして実行できるようになります。
-fは続くファイルをmakefileとして読み込むオプションです。
しかし、shebangには仕様的な制限がありオプションを二つ以上書く事が出来ません。つまり上で紹介した「make -j -f」の様にデフォルトで並列処理を有効にすることが出来ないのです。
そこでそれを回避する方法を使います。
#!/bin/bash
make -j -f <(tail -n+4 $0) $@ ;exit 0
all:
echo Hello World
上記の様に書くと、一旦はシェルスクリプトとして認識した後、自らの4行目以降をGNU makeに渡す事ができます。これでMakefileを好きな名前にしてjobserverモードでGNU makeで実行出来る様になりました。
/tmpフォルダで実行し一時ファイルを自動的に消す
GNU makeでスクリプトを書くと、ファイルを介して情報をやり取りすることが多くなります。
その時に使う一時ファイルはカレントディレクトリに作られるので、カレントが書き込み可能でなければいけません。できれば/tmpなどのテンポラリディレクトリで実行し、一時ファイルは終了後消したいものです。
以下はそれを実現します。
#!/bin/bash
EX=$(readlink -f $0);cd /tmp;make -j -f <(tail -n+4 $EX) $@ ;exit 0
all:tempfile
echo Hello World
.INTERMEDIATE:tempfile
tempfile:
touch tempfile
一旦/tmpにcdしてから覚えておいたパスからスクリプトを読み込みmakeを実行しています。
.INTERMEDIATEの指定は一時ファイルをGNU makeに教えます。指定した一時ファイルはスクリプト実行後に自動的に削除されます。
readlinkはスクリプトの絶対パスを取得する為に実行します。MacOSなどでは標準で使えないので単に「EX="$0"」としてもよいでしょう(この場合、相対パスでの実行は出来なくなります)
便利なオプション
-jの後に数値をつけると、同時に実行するタスク数を制限できます。例えば「make -j2」だと、2つまで同時に実行します。サーバー接続数に制限がある場合などに便利です。
-sオプションをつけるとサイレントモードになり、コマンドの表示が抑制されます。よりスクリプトライクになります。
おわりに
1977年に開発されたGNU makeにjob server機能が追加されたのは1999年です。そこから15年以上、未だこれほど手軽に並列処理スクリプトを実行できる環境は少ないのではないでしょうか。
ant/rake/cake/gruntなどビルドツールとしては後発のものが存在しますが、記述の簡潔さ・並列処理の可能性・スクリプトへの応用性おいては、やはりGNU makeが最強であると筆者は考えます。