Makefileと並行taskと生成されたtaskの実行の悩み
はじめに
make に -j
オプションを渡してあげると、良い感じにmakeが並行実行してくれる。
default: do01 do02 do03 do01: @echo start 01 sleep 1 @echo end 01 do02: @echo start 02 sleep 1 @echo end 02 do03: @echo start 03 sleep 1 @echo end 03
-j
を付けない場合。3秒掛かる。
$ time make start 01 sleep 1 end 01 start 02 sleep 1 end 02 start 03 sleep 1 end 03 real 0m3.057s user 0m0.011s sys 0m0.029s
-j
を付けた場合。1秒で終わる。
$ time make -j 4 start 01 sleep 1 start 02 sleep 1 start 03 sleep 1 end 02 end 03 end 01 real 0m1.027s user 0m0.013s sys 0m0.021s
-j
をつければ、do01,do02,do03のtaskが並行して実行されるので1秒で済む。つまり複数のtaskを同時にやりたければ、そしてそれをmakeに任せるならtaskを定義しなくてはいけない。
並行実行できない処理(良くない)
当然だけれど。以下のような書き方では並行にtaskが実行できない。
内部でシェルのループが動いているだけ。
default: for i in `jot 3 1`; do echo start $$i; sleep 1; echo end $$i; done
同様にこれもダメ。
タスクが区切られていないのでシーケンシャルに実行されていくだけ。
default: @echo start 01 sleep 1 @echo end 01 @echo start 02 sleep 1 @echo end 02 @echo start 03 sleep 1 @echo end 03
並行実行できるにはできるけれど微妙なもの
(こういう書き方もあるけれど微妙)
run.bash
#!/bin/bash echo $1 sleep 1 echo end $1
default: ./run.bash 01& ./run.bash 02& ./run.bash 03& wait
(ちなみに1行で書かないと上手くwaitが機能してくれない。それはそう)
なるべく並行taskの数はmake側で制御したい
なるべく並行taskの数はmake側で制御したい。というかmakeにおまかせしたい。
そうなると結局、個別の出来事毎にtaskを作らざる負えない。
taskの生成
makeをタスクランナー的に使っている場合に、複数のファイルに対して全部同じ処理を行いたい、ただし並行に実行されて欲しいということがある。このような時にtaskの生成が欲しくなる。
方法としては2つある。
makeの知識を必要としないという意味では前者が良いかもしれない。一方で無駄な中間ファイルなどができる。 makeで完結するという意味では後者が良いかもしれない。しかしちょっと読むのがむずかしいMakefileになってしまうような気がする。
それぞれのの仕方についてメモしておく。
Makefileを生成
こちらは何らかの方法を使ってMakefile自体を生成する方法。例えば、jinja2などのtemplate engineを使ってMakefile(run.mk)を生成する。 トップレベルのMakefileからは生成されたMakefile(run.mk)を呼び出すようにする
default: run.mk $(MAKE) -f run.mk clean: rm -f *.mk run.mk: kamidana run.mk.j2 > run.mk
自分たちが慣れているtemplate engineの機能を使ってMakefileを作る分良いような気がしている。
run.mk.j2
do: {% for i in range(1, 4) %}do0{{i}} {% endfor %} {% for i in range(1, 4) %} do0{{i}}: @echo start 0{{i}} sleep 1 @echo end 0{{i}} {% endfor %}
以下のようなrun.mkが生成され利用される。
do: do01 do02 do03 do01: @echo start 01 sleep 1 @echo end 01 do02: @echo start 02 sleep 1 @echo end 02 do03: @echo start 03 sleep 1 @echo end 03
$ time make -j 4 kamidana run.mk.j2 > run.mk make -f run.mk start 01 sleep 1 start 02 sleep 1 start 03 sleep 1 end 02 end 03 end 01 real 0m1.248s user 0m0.191s sys 0m0.057s
細かい話(taskを登録するかbuild対象のファイルを登録するか)
ただ、少し面倒くさい部分があるかもしれない。元々Makefileはbuild時の依存関係を記述するものなのでタスクランナーではない。
$ make run.mk make: `run.mk' is up to date. # (もちろん、 `make -B -j 4` など `-B` をつければ再度呼ばれる(always-make)) $ make -B run.mk kamidana run.mk.j2 > run.mk
そんなわけで、先程の例の用に run.mk
をbuild対象のファイルとして指定した場合に、既に run.mk
が存在していれば生成されなくなる。
常に最新の値で何度でもbuildしたい場合には生成されるファイルをMakefileに記述しない方が良いかも知れない(例えば、genRunMakeというtask名などにし.PHONYを付加する)。
Makefileの中でtaskを生成
Makefile中でtaskを生成することもできる。プリプロセッサの様なイメージ。とは言え、こちらのMakefileはMakefile読者中級以上の知識を必要とするような気がする(中級という級位に特に意味はないけれど)。
これには、foreach,eval,make,shellを使う*1。
NUMS = $(shell jot 3 1 | sed 's/^/0/g') default: $(foreach i,${NUMS},do$i) define _genTask do$1: @echo start $1 sleep 1 @echo end $1 endef $(foreach i,${NUMS},$(eval $(call _genTask,$i)))
短い事は短いのだけれど、callで文字列置換的にタスクを生成し、それをevalで解釈するというループ(foreach)を記述するということが理解できるかどうか。読める人は読めるのだけれど。ちょっと解釈が難しい記述になってしまう。
もちろん、並行で動作する。
$ time make -j 4 start 01 start 02 sleep 1 sleep 1 start 03 sleep 1 end 02 end 03 end 01 real 0m1.070s user 0m0.023s sys 0m0.056s
結論はまだ出ていない
結論はまだ出ていない。あんまりmakeに深い入りするのもどうかと思う一方で生成のフローが煩雑になるというのも良くない。ただ並行数の制御はmakeに任せたい。
*1:substやwildcardなども使うかもしれない