スレッドの落とし穴(5/6 ページ)

» 2005年03月23日 20時24分 公開
[石井宏治,ITmedia]
  • Interlockedクラス

 あるニュースグループで「lockにタイムアウトを指定できればよい」という提案があった。一定時間以内にlockできなければ諦める、というロジックだ。

 ほかのスレッドが何かをしている場合に取り得る別の手段がある場合には、Interlockedクラスを使おう。


	private int SyncObject;
	public void DoSomething() {
		if (Interlocked.CompareExchange(
			ref this.SyncObject,
			1, 0) != 0)
			return;

 分割不可能な操作として比較と設定を行ってくれるため、このコードはスレッドセーフになる。一定時間待つなどの高度な機能は持たないが、Interlockedはlockなど、ほかの手段と比べてパフォーマンスに与える影響が非常に小さい。ほかの手段は、相当数のコードが実行されるが、Interlockedによって増えるコードはx86においてはlockプレフィックスの1バイトだけだ。

 このためまずは、Interlockedクラスで用が足りないか検討し、足りない場合にのみほかの解決策を検討するべきだ。

  • ReaderWriterLockクラス

 逆にもっと多機能なスレッド同期が必要な場合には、ReaderWriterLockクラスがよい。

 このクラスはlock/SyncLockが内部的に利用するMonitorクラスと比べて大きく以下の利点を持つ。

1. 読み込みロックと書き込みロックを区別することで、複数スレッドからの同時読み込みをサポートすることができる。

2. ロック数にかかわらず、現在のスレッドが持っているロックを完全に開放することができる。

 最初に挙げた利点は、並列性を上げることでパフォーマンスの向上に役立つ。

 次の利点は、より安全なコードに向けた配慮ができる。サンプルコードでその違いを見てみよう。C#におけるlock、あるいはVB.NETにおけるSyncLockは、内部では以下のコードに展開される。


	Monitor.Enter(obj);
	try {
		...
	} finally {
		Monitor.Exit(obj);
	}

 十分に安全そうに思えるが、数少ない例外としてfinally節が実行されない場合がある。例えばStackOverflowExceptionが発生した場合には、finally節は実行されない。

 筆者は、StackOverflowExceptionなどは、バグで無限再帰にでもならない限り起きないよ、と思っていたが、実はそうでもないことを最近になって身をもって知った。ADO.NETを使った以下のコードを見てほしい。


	DataView view = new DataView(table);
	lock (table) {
		view.RowFilter = filter;
		...
	}

 DataView.RowFilterに式を設定すると、ADO.NETの中でその式の解析が行われるが、そこで再帰が使われているため、ORやANDをたくさん並べた複雑な式の場合にはこの行でStackOverflowExceptionが発生する。

 その場合、finally節が実行されないためlockが解放されずにスレッドが終了してしまうため、ほかのスレッドはlockを永久に取得できなくなる。プログラムとしては、ハングアップの状態だ。

 問題の根本としては、lockがtry/finallyに展開されることと、finallyが常に実行されるわけではないことに起因している。このため、lockやtry/finallyに依存せずに、Monitorとtry/catchを用いて書けば避けることができるのだ。実際、lock/SyncLockがtry/catchに展開されないのは、仕様として間違っているのではないかとも思われる。次のコードは安全だ。


	DataView view = new DataView(table);
	Monitor.Enter(table);
	try {
		view.RowFilter = filter;
		...
		Monitor.Exit(table);
	} catch {
		Monitor.Exit(table);
	}

 しかし、どこでStackOverflowExceptionが発生するのかは事前には分からない。より安全なコードとして、上位でスレッドが終了する時にそのスレッドが持っているロックをきちんと開放してくれれば、プログラムにほかのバグがあった時でも、被害を抑えることができる。


	private void ThreadMain() {
		try {
			...
			rwlock.ReleaseLock();
		} catch {
			rwlock.ReleaseLock();
		}
	}

 ReaderWriterLock.ReleaseLockメソッドは、ロックの取得回数にかかわらず、現在のスレッドで保持しているロックを解放してくれる。

 ロック保持状態を取得するIsReaderLockHeldやIsWriterLockHeldプロパティもあるため、ロックしたまま終了しよとするスレッドがあれば、Assertを起こすことでバグの早期発見に役立てることもできるだろう。

 小さいクラス内のスレッド同期には向かないかもしれないが、スレッドを利用するアプリケーションレベルではぜひ入れておきたい安全策だ。

Copyright © ITmedia, Inc. All Rights Reserved.

注目のテーマ