xargs -P と export -f を使ってシェルスクリプトで並列処理を実現する

xargs-Pオプションを使うとパイプで渡された値を任意の並列数で並列処理することができます。以下では、普通に実行すると10秒かかる処理が、5並列で並列処理することによって2秒で実行することができます。

f.sh:

#!/bin/bash
echo $*;
sleep 1;
$ seq 10 | xargs -I % -P 5 ./f.sh %
1
2
3
4
5
6
7
8
9
10

そのため、シェルスクリプトで大量の項目をforを使って処理している部分をxargsに置き換えることによって高速化することができます。

しかし、xargsでは関数を使用できないので、このままでは並列処理したい部分を都度別のシェルスクリプトに分割しなくてはなりません。

$ function f { echo $*; sleep 1; }
$ seq 10 | xargs -I % -P 5 f %
xargs: f: No such file or directory
$ echo 'echo $*; sleep 1;' > f.sh
$ seq 10 | xargs -I % -P 5 sh ./f.sh %
1
2
3
4
5
6
7
8
9
10

ですが、bashにはexport -fという関数をエクスポートできる機能があり、これとbash -cを組み合わせることによって、いちいち並列処理をしたい部分をシェルスクリプトに書き出さなくてもよくなります。

$ function f { echo $*; sleep 1; }
$ export -f f
$ seq 10 | xargs -I % -P 5 bash -c 'f %'
1
2
3
4
5
6
7
8
9
10

しかし、xargs-Iオプションを使うとデリミタが改行になってしまうので、以下のような場合は意図した動作になりません。

$ function f { echo $*; sleep 1; }
$ export -f f
$ echo '1 2 3 4 5 6 7 8 9 10' | xargs -I % -P 5 bash -c 'f %'
1 2 3 4 5 6 7 8 9 10

この場合は、xargs-dオプションを使うことでデリミタを指定できるので、上記の場合はスペースを指定すれば意図した動作になります。

$ function f { echo $*; sleep 1; }
$ export -f f
$ echo '1 2 3 4 5 6 7 8 9 10' | xargs -d ' ' -I % -P 5 bash -c 'f %'
1
2
3
4
5
6
7
8
9
10

しかし、このオプションはGNU版のxargsにしかないので、Mac(BSD系)だと動きません。なので、brew install findutilsしてgxargsを入れて、alias xargs=gxargsとする必要があります。またaliasインタラクティブシェルでないと動作しないので、シェルスクリプトの場合は、さらにshopt -s expand_aliasesとする必要があります。

a.sh:

#!/bin/bash

# for Mac(BSD系)
shopt -s expand_aliases
alias xargs=gxargs

function f {
  echo $*;
  sleep 1;
}
export -f f
echo '1 2 3 4 5 6 7 8 9 10' | xargs -d ' ' -I % -P 5 bash -c 'f %'
$ ./a.sh
1
2
3
4
5
6
7
8
9
10