PHP5でファイルをロックするコードを書いてみた

友人のmezawa氏から、トラックバックが届いた。ブログを始めた様子(リンクはこちら)。


(このログの流れからすると)唐突だが、PHP5でファイルをロックする仕組みを書いてみた。

PHPプログラムでファイルをロックするには、fopen()でファイルポインタを取得したのち、flock()を使う、という道具が用意されている。
ただし、wやrといったモードでオープンする際などでは、独自実装をする必要がある(参考)。

PHPには、(ファイルシステム上の)ファイルをハンドルするための関数系がキチンと用意されているので、「モードに関係なく、ロックしたくなったらしよう」というプログラムを書いてみた。この方がスッキリしそうだ。

ファイルをロックするには、(ロックするファイルと同一)ディレクトリにロックファイルを作成して対処する。
ロックしようと思ったら、

  • ロックファイルを作成して、ファイルをオープンする。
  • 上の際に、ロックファイルが存在したら、一定時間だけ待機してリトライする。この「一定時間(タイムアウト時間)は指定可能にする。
  • ロックが不要になったら、ロックファイルを削除して、ファイルをクローズする。

という仕様にした。なので、ファイルの存在するディレクトリには、書き込み権限を与えておく必要がある。

以下が、プログラム(BaseTextDao.class.php)。主要なメソッドは以下。

getLock() ロックファイルを生成する。
releaseLock() ロックファイルを削除する。
fileLockOpen() ロックファイルを生成して、ファイルをオープンする。
fileOpen() aとr+モードの場合、fileLockOpen()を使ってオープンする。以外のモードでは、ロックしない。
fileLockClose() fileLockOpen()で開いたファイルをクローズする。
fileClose() fileOpen()で開いたファイルをクローズする。
<?php
/**
 * BaseTextDao.class.php
 * 
 * (C) 2009, tetsuya.odaka(EzoGP).
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* 
 * textファイル操作のためのDao
 * 
 * 
 * author;	t.odaka
 * date ;	2009/5/6
 * 
 */

class BaseTextDao {
	/*
	* プロパティー 
	*/
	// ファイル名(パス付き)
	var $FileName;
	var $LockFileName;
	
        /* 
	 * デフォルトのコンストラクタ 
	 */
	function __construct() {
	}

	/* 
	 * Getter 
	 */
	function getFileName(){return $this->FileName;}
	function getLockFileName(){return $this->LockFileName;}
	
	/* 
	 * Setter 
	 */
	function setFileName($_str){
		$this->FileName = $_str;
		// デフォルトのロックファイル
		$this->LockFileName = $this->FileName.'.lck';
		return;
	}
	
	// 特別にロックファイルを設定した場合
	function setLockFileName($_str){
		if($_str){
			$this->LockFileName = $_str;
		}
		return;
	}

	/*
 	* getLock
 	*   this->FileNameのロックファイルを作成します。
 	* 	ファイルロックが、タイムアウト値までに取得できなければ、失敗となります。
	*   パラメータ
	* 	$_tout	:   タイムアウト値(milisec)
 	*  	戻り値	:	ロックファイルを生成できたらtrue、失敗したらfail。
 	*/
	function getLock($_tout){
	
		if(!is_numeric($_tout)) return null;
		if($_tout < 0) return null;
		
		// タイムアウトするループ回数
		$_eol = round($_tout/5000);
		
		// lockファイル名
		$_lckfn = $this->LockFileName;
		
		// ループカウンタ
		$_lcnt = 0;
		
		while($_lcnt < $_eol){
			if(!file_exists($_lckfn)){
				// ロックファイルの生成
				if(touch($_lckfn)){
					// ファイルオープン
					return true;
				}else{
					return false;
				}
			}
			// 0.005 秒待つ
			usleep(5000);
			$_lcnt++;
		}
		
		return false;
	}

	
	/*
 	* releaseLock
 	*   ロックファイルを削除して、ファイルロックを解除します
 	* 	ファイルロックが、1秒間解除できなければ、失敗となります(無限ループ防止)。
	* 	パラメータ
	* 	戻り値	:   ロックファイルを削除したらtrue、失敗したらfalse。
 	*/
	function releaseLock(){
		
		// lockファイル名
		$_lckfn = $this->LockFileName;
	
		// ループカウンタ
		$_lcnt = 0;
		while($_lcnt < 20){
			if(file_exists($_lckfn)){
				// ロックファイルの削除
				if(unlink($_lckfn)){
				return true;
				}
				return false;
			}
			// 0.05 秒待つ
			usleep(50000);
			$_lcnt++;
		}
		return false;
	}
	
	/*
 	* fileLockOpen
 	*   this->FileNameをファイルロックし、ファイルポインタを返却します。
 	* 	ファイルロックが、タイムアウト値までに取得できなければ、失敗となります。
	*   パラメータ
	* 	$_opt	:   'r','r+','a','w',...
	* 	$_tout	:   タイムアウト値(milisec)
 	*  	戻り値	:   ロックファイルを生成し、ファイルをオープンできたらファイルポインタ、失敗したらnull。
 	*/
	function fileLockOpen($_opt, $_tout){
	
		// ファイルロックの獲得
		if($this->getLock($_tout)){
		// ファイルオープン
			$_fp = fopen($this->FileName, $_opt);
			if(!$_fp) return null;
		}else{
			return null;
		}
		
		return $_fp;
	}
	
	/*
 	* fileOpen
 	*   this->FileNameのファイルポインタを取得します
 	*   ファイルが、a, r+の場合にはファイルロックをして、ファイルを開きます。
	* 	パラメータ
	* 	$_opt	:   'r','r+','a','w'
 	*  	戻り値	:	正常にファイルをオープンできたらファイルポインタ、失敗したらnull。
 	*/
	function fileOpen($_opt){

		if($_opt == 'r+' || $_opt == 'a'){
			// ファイルオープン
			$_fp = $this->fileLockOpen(1000000);
			if(!$_fp) return null;
		}else{
			// ファイルオープン
			$_fp = fopen($this->FileName, $_opt);
			if(!$_fp) return null;
		}
		return $_fp;
	}
	
	/*
 	* fileLockClose
 	*   ファイルロックを解除して、ファイルをクローズします
 	* 	ファイルロックが、1秒間解除できなければ、失敗となります(無限ループ防止)。
	* 	パラメータ
	*   $_fp	:	ファイルポインタ
	* 	戻り値	:   ロックファイルを削除し、ファイルをクローズしたらtrue、失敗したらfalse。
 	*/
	function fileLockClose($_fp){
		// ロックファイルの削除
		if($this->releaseLock()){
			// ファイルクローズ
			if(fclose($_fp)) return true;
			return false;
		}else{
			return false;
		}
	}
	
	/*
 	* fileClose
 	*   ファイルポインタを受け取ってファイルをクローズします。
 	*   ファイルが、a, r+の場合にはファイルロック解除して、ファイルを閉じます。
	*   パラメータ
	*   $_fp	:	ファイルポインタ
	* 	$_opt	:   'r','r+','a','w'
	*   戻り値	:	ファイルが正常にクローズできたらtrue、失敗したらfalse。
 	*/
	function fileClose($_fp, $_opt){
		if($_opt == 'r+' || $_opt == 'a'){
			// ファイルクローズ
			if(!$this->fileLockClose($_fp)) return false;
		}
		// ファイルクローズ
		if(!fclose($_fp)) return false;
		return true;
	}
}
?>


以下が、このクラスを使うコードのサンプル。

<?php
/* 
 * BaseTextDao.class.phpのテスト
	      
		author	; t.odaka
		date	; 2009/5/4
*/
	
	require_once("BaseTextDao.class.php");

	$pObj = new BaseTextDao();

        // data/pref.txtをロックしてオープンする。
	$pObj->setFileName('data/pref.txt');
	
	// ロックを取得して、'r'モードでファイルをオープン
        // タイムアウト時間は少数も可。この場合、999999.2/50000を四捨五入して整数化した数
        // だけ、0.05秒おきにリトライする。
	$fp = $pObj->fileLockOpen('r',999999.2);
	
	// ロックを解放して、ファイルをクローズする。
	$pObj->fileLockClose($fp);

	return;
?>