セマフォを使った、同時実行の重複数を制限するサンプル (Perl 5.005 Later)

先日書いた Perl スクリプトで、システムリソースを多めに食いがちな処理があったので、この箇所を同時に実行するプロセス本数を制限するようにしようと思いました。普通、この手の排他処理はセマフォ(SystemV 系プロセス間通信機能のひとつ)を使うのが定石だよな、と Google 先生にお伺いを立てたところ(久々にセマフォ処理を書くので)、意外に情報が少なかったんですよね。Thread::Semaphore モジュールを使ったマルチスレッド間での排他処理関係はそれなりに見かけたんですが... もしかしたら、別に今風のやり方があるのかしら? とまれ、セマフォ関数はシステムコールのインターフェースそのままの、初心者には使いやすいとは言えない代物ですし、備忘録も兼ねてコードのサンプルを書き残しておくことにしました。

まずは排他処理をイメージ化したものを以下に。レジが3つしかないお店でレジがお客でいっぱいになったら、レジ待ちの行列が出来ますわな。で、行列の先頭の人はどっかのレジが空いたらすかさずそこにスルッと進む、という図を想像してみて下さい。(下図)。

つまり、「お客」が perl スクリプトを実行したプロセスで、「レジ」がスクリプト中の同時実行数を制限したい処理という訳です。

以下がスクリプトセマフォ関連処理だけ抜き取ったものです。

#!/usr/bin/perl
use strict;
use IPC::SysV;
use Getopt::Long;
use utf8;
binmode(STDOUT,':utf8');

# 定数
use constant {
	DUP_MAX => 3,			# 同時実行数の最大許容値
	SEM_KEY => 1234,		# セマフォキー(システム中で一意)
	IPC_CREAT => 0001000,
	IPC_RMID => 0,
	GETVAL => 12
};

# パラメータ処理
my $USAGE = 'usage : semtest.pl [ --init ] [ --query ]';
my %opts = (
	init => 0,
	query => 0
);
my $ret = GetOptions(
	\%opts,
	#セマフォ初期化
	'init',
	#セマフォ値問合せ
	'query'
);

#セマフォ初期化 (セマフォを生成し、初期値=同時実行最大数をセット)
if ( $opts{init} ) {
	my $semid = semget( SEM_KEY, 0, 0 );
	if ( $semid ) {
		semctl( $semid, 0, IPC_RMID, 0 ); #セマフォ削除
	}
	$semid = semget( SEM_KEY, 1, 0666 | IPC_CREAT ) || die 'semget (IPC_CREAT) error';
	my $semnum = 0;
	my $semflg = 0;
	my $operation = pack( "s!s!s!", $semnum, DUP_MAX, $semflg );
	semop( $semid, $operation ) || die "semop (Init) error";
	exit;
}

#セマフォ値問合せ
if ( $opts{query} ) {
	my $semid = semget( SEM_KEY, 0, 0 ) || die 'semget error';
	my $semval = semctl( $semid, 0, GETVAL, 0 );
	print "セマフォの値 : $semval\n";
	exit;
}

my $semid = semget( SEM_KEY, 0, 0 ) || die 'cannot semget';
print "$$ : 起動\n";

# セマフォ Down (セマフォの値が1以上の場合はすぐに、1未満なら1以上になるまで待ってから、セマフォ値を1だけ減少させる)
my $semnum = 0;
my $semope = -1;
my $semflg = 0;
my $operation = pack( "s!s!s!", $semnum, $semope, $semflg );
semop( $semid, $operation ) || die "semop (Down) error";

print "$$ : 重複限定処理 開始\n";
# ここに重複を限定する処理を書く
print "$$ : 重複限定処理 終了\n";

# セマフォ Up (セマフォの値が1だけ増加させる)
$semope = 1;
$operation = pack( "s!s!s!", $semnum, $semope, $semflg );
semop( $semid, $operation ) || die "semop (Up) error";
print "$$ : 終了\n";

だいたい分かりますよね? 前提として、セマフォは使用前にユニークなキーを付けて生成し、所定の値をセットしておく必要があります。なので保守用として、セマフォの初期化およびセットされている値を問合せて表示する機能も含めました。それぞれ、--init, --query というスイッチを付けて実行します。