Unix/Linuxにおけるプロセスレベルでの並列処理の実現(xargs利用)


はじめに

マルチコアのCPUを持つサーバにおいて、CPUリソースを最大限に活かして複数のプログラムを実行したい場合がある。
この問題を解決する為にxargsコマンドを利用した解法をその一例として紹介する。

この解法はなるべく既存のプログラムに手を加える事無く並列化を実現したい場合に有効。

xargsコマンドとは

既に基本的な使い方を知っている方はこの章は読み飛ばして頂きたい。

任意のコマンド(プログラム)に対して標準入力経由で引数を与え、実行する事ができるコマンド。それがxargsである。
言葉ではなかなか伝わらないので、まずは、以下のサンプルコードを見て頂きたい。

sample1
% ls -l
total 16
-rw-r--r-- 1 root root   4 Feb 19 18:31 aaa.txt
-rw-r--r-- 1 root root   4 Feb 19 18:31 bbb.txt
-rw-r--r-- 1 root root   4 Feb 19 18:31 ccc.txt

% echo aaa.txt bbb.txt ccc.txt | xargs zip archived_txt.zip
  adding: aaa.txt (stored 0%)
  adding: bbb.txt (stored 0%)
  adding: ccc.txt (stored 0%)

% ls -ltr
total 16
-rw-r--r-- 1 root root   4 Feb 19 18:31 aaa.txt
-rw-r--r-- 1 root root   4 Feb 19 18:31 bbb.txt
-rw-r--r-- 1 root root   4 Feb 19 18:31 ccc.txt
-rw-r--r-- 1 root root 460 Feb 19 18:32 archived_txt.zip

上記ではaaa/bbb/ccc.txtの3ファイルをzipコマンドでアーカイブしている。
zipコマンドのみで実行する場合は、% zip archived_txt.zip aaa.txt bbb.txt ccc.txt となるが、標準入力の内容をそのまま引数として扱える事がポイントだ。

デフォルトでは空白や改行文字で要素を区切るので、以下のような指定も可能。

sample2
% cat txt.lst
aaa.txt
bbb.txt
ccc.txt

% cat txt.lst | xargs zip archived_txt.zip
  adding: aaa.txt (stored 0%)
  adding: bbb.txt (stored 0%)
  adding: ccc.txt (stored 0%)

この使い方も結構便利。
区切り文字は-dで変更できるためカンマ区切りで受け渡すこともできる。

xargsコマンドを利用したコマンドの並列実行

若干脱線気味だが、ここからがポイント。

xargsは-nオプションを利用すると、コマンドに引き渡す引数の数を制限する事ができる。
このオプションによって引数の数の制限を超えた場合は、複数回に分けてコマンドが実行される。

sample3
% echo aaa bbb ccc ddd | xargs echo
aaa bbb ccc ddd

% echo aaa bbb ccc ddd | xargs -n 2 echo
aaa bbb
ccc ddd

% echo aaa bbb ccc ddd | xargs -n 1 echo
aaa
bbb
ccc
ddd

そして、この複数回に分けてコマンドを実行する場合に、-Pオプションを併用すると、
xargsは-Pで指定した数のプロセスを複数生成し、それぞれのプロセスにコマンドを実行させる事ができる。

この時、CPUのリソースを最大限に活かすのであれば、-PにCPUコア数を与えればよい。
※x86のハイパースレッディングのように、コアを更に仮想的に分割しているシステムの場合、コマンド(プログラム)の特性にもよるが上限はスレッド数ではなくコア数とした方が良いかもしれない。

次の章で実際に並列処理がなされている事を確認する。

並列処理サンプルコード

サンプルコードはコマンド毎に実行時間にばらつきが出る様にしている。
あるコマンドが早く終了すれば、すかさず新しいプロセスが生成され常に指定した数のプロセス(2プロセス)が実行されている事が分かる。

hoge.sh
#!/bin/bash

#
# prepared.
################################################################################
rand=`od -An -N1 -d /dev/random | tr -d '[:blank:]'`
sleep_time=`expr ${rand} % 10 + 1` # create a number in the range of 0 to 10.

#
# main.
################################################################################
before=`date '+%Y-%m-%d %H:%M:%S'`
echo "${before} >>> start ${0} (PID: $$, argv: ${1}, sleep_time: ${sleep_time})"
sleep ${sleep_time}
after=`date '+%Y-%m-%d %H:%M:%S'`
echo "${after} <<<   end ${0} (PID: $$, argv: ${1}, sleep_time: ${sleep_time})"
実行結果
% echo "1 2 3 4" | xargs -n 1 -P 2 ./hoge.sh
2016-02-19 20:01:06 >>> start ./hoge.sh (PID: 32448, argv: 1, sleep_time: 5)
2016-02-19 20:01:06 >>> start ./hoge.sh (PID: 32449, argv: 2, sleep_time: 7)
2016-02-19 20:01:11 <<<   end ./hoge.sh (PID: 32448, argv: 1, sleep_time: 5)
2016-02-19 20:01:11 >>> start ./hoge.sh (PID: 32463, argv: 3, sleep_time: 1)
2016-02-19 20:01:12 <<<   end ./hoge.sh (PID: 32463, argv: 3, sleep_time: 1)
2016-02-19 20:01:12 >>> start ./hoge.sh (PID: 32471, argv: 4, sleep_time: 8)
2016-02-19 20:01:13 <<<   end ./hoge.sh (PID: 32449, argv: 2, sleep_time: 7)
2016-02-19 20:01:20 <<<   end ./hoge.sh (PID: 32471, argv: 4, sleep_time: 8)

上記の例ではhoge.shに引数を渡す形をとっているが、以下の様に実行するファイル名を直接渡すような利用方法も考えられる。

sample4
% echo "aaa.sh bbb.sh ccc.sh" | xargs -n 1 -P 2 sh

xargsすばらしい!

最後に

-Pオプションを持っているxargsはGNUのxargsのようで、昔仕事でよく利用していたSolaris10には標準では用意されていなかった。
参考URLにも載せたが最新のSolaris11には標準で用意されていた。良かった良かった。

参考URL

https://linuxjm.osdn.jp/html/GNU_findutils/man1/xargs.1.html
http://docs.oracle.com/cd/E53394_01/html/E54763/xargs-1g.html#scrolltoc
https://www.freebsd.org/cgi/man.cgi?query=xargs