ホームに戻る
出典 :
関連 :
目次 :
lock ステートメントによる排他制御
任意のオブジェクトを用いて簡便に相互排他を行うことができる。
あるスレッドがロックを取得している間に、他のスレッドが同一のオブジェクトを用いてロックを取得しようとした場合はブロックされ、先にロックを取得したスレッドがロックを解放するまで待ち状態となる。
機能としてはバイナリセマフォと同等である。後述のセマフォよりもパフォーマンス面で有利。

一般形
lock ステートメント中に、排他実行したい処理を記述する。
public class Sample
{
// ロックオブジェクト
private object obj_A;
// lock ステートメントを含む DoSomething() メソッド
private void DoSomething()
{
// lock ステートメント
lock( obj_A )
{
// 排他実行したい処理
}
}
}
lock ステートメントと例外
lock ステートメントは以下のコードと等価である。
object __lockObj = obj_A;
bool __lockWasTaken = false;
try
{
System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken);
// 排他実行したい処理
}
finally
{
if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj);
}
try - finally ブロックが使用されているため、例外が発生した場合でもロックが解放される(例外安全)。
注意すべき点
-
複数のスレッドがロックの解放を待機している場合、ロックの獲得順は待ち順と一致しない(FIFOではない)。
-
lock ステートメントを記述する際、ロックオブジェクトに以下を使用することは禁忌である。
- this
- Type 型インスタンス
- 文字列インスタンス(リテラルを含む)
-
lock ステートメント中に await を使用することはできない。
lock ステートメントでは複数のスレッドが共通のロックオブジェクトを同時に占有することはできないが、セマフォを用いると指定された数のスレッドによる同時占有(エントリ)が可能となる。
System.Threading.Semaphore
標準のセマフォクラス。リソースに同時にアクセスできるスレッドの本数を制限できる。
using System;
using System.Threading;
public class Example
{
// Semaphore クラスインスタンス
private static Semaphore _pool;
:
// メインメソッド
public static void Main()
{
// セマフォの初期化
// 初期カウント : 0
// 最大カウント : 3
Console.WriteLine("メインスレッド開始、セマフォを初期化");
_pool = new Semaphore(initialCount: 0, maximumCount: 3);
// 5 本のスレッドを起動
for(int i = 1; i <= 5; i++)
{
Thread t = new Thread(new ParameterizedThreadStart(Worker));
t.Start(i);
}
:
// セマフォを 3 回解放
Console.WriteLine("メインスレッドがセマフォを解放");
_pool.Release(releaseCount: 3);
Console.WriteLine("メインスレッド終了");
}
// ワーカースレッド
private static void Worker(object num)
{
// セマフォの空きを待機する
Console.WriteLine($@"スレッド #{num} 開始、セマフォを待機");
_pool.WaitOne();
// セマフォに空きが発生したので、待ちを解除
Console.WriteLine($@"スレッド #{num} がセマフォを獲得");
:
// セマフォを解放
Console.WriteLine($@"スレッド #{num} がセマフォを解放");
_pool.Release();
}
}
出力結果 :
メインスレッド開始、セマフォを初期化 ( セマフォカウント : 0 / 3 )
スレッド #1 開始、セマフォを待機 ( セマフォカウント : 0 / 3 )
スレッド #2 開始、セマフォを待機 ( セマフォカウント : 0 / 3 )
スレッド #3 開始、セマフォを待機 ( セマフォカウント : 0 / 3 )
スレッド #4 開始、セマフォを待機 ( セマフォカウント : 0 / 3 )
スレッド #5 開始、セマフォを待機 ( セマフォカウント : 0 / 3 )
(セマフォが解放されるまでワーカースレッドは待機する)
メインスレッドがセマフォを解放 ( セマフォカウント : 0 ⇒ 3 / 3 )
メインスレッド終了
スレッド #2 がセマフォを獲得 ( セマフォカウント : 3 ⇒ 2 / 3 )
スレッド #5 がセマフォを獲得 ( セマフォカウント : 2 ⇒ 1 / 3 )
スレッド #4 がセマフォを獲得 ( セマフォカウント : 1 ⇒ 0 / 3 )
(セマフォを獲得できなかったワーカースレッドはセマフォが解放されるまで待機する)
スレッド #2 がセマフォを解放 ( セマフォカウント : 0 ⇒ 1 / 3 )
スレッド #3 がセマフォを獲得 ( セマフォカウント : 1 ⇒ 0 / 3 )
スレッド #5 がセマフォを解放 ( セマフォカウント : 0 ⇒ 1 / 3 )
スレッド #1 がセマフォを獲得 ( セマフォカウント : 1 ⇒ 0 / 3 )
スレッド #4 がセマフォを解放 ( セマフォカウント : 0 ⇒ 1 / 3 )
スレッド #3 がセマフォを解放 ( セマフォカウント : 1 ⇒ 2 / 3 )
スレッド #1 がセマフォを解放 ( セマフォカウント : 2 ⇒ 3 / 3 )
ここでは、セマフォカウントの最大が 3 のセマフォを用いる例を示している。
Main() メソッド内でセマフォを初期化、ワーカースレッドはセマフォを獲得( WaitOne() )しようとしているが、初期カウントが 0 (すべて獲得済み)のため待ちとなる。
Main() メソッド内でセマフォを解放( Release() )すると、ワーカースレッドがセマフォを獲得可能となる。このとき、引数に指定した回数だけセマフォから出る。
最大3本のスレッドが同時にエントリでき、セマフォを獲得できなかったワーカースレッドは引き続きセマフォの空きを待機する。
ここで、複数のスレッドがセマフォを待機している場合、獲得順は待ち順と一致しない(FIFOではない)。
また、lock ステートメントと同様本文中で await を使用できない。パフォーマンスは後述の SemaphoreSlim よりも不利である。
応用
Semaphore クラスは初期化時にセマフォの名称を指定できる。これにより、他のプロセスと共有される名前つきシステムセマフォを作成することができる。
利用法のひとつとして、アプリケーションの多重起動抑止がある。リンク先を参照。
System.Threading.SemaphoreSlim
Semaphore をより軽量に改良したもの。使用法は同様。
システムセマフォを作成して他プロセスと共有することはできないが、同一プロセス内でのロックに用いるならば Semaphore よりもパフォーマンス面で有利。
また、Semaphore と異なり本文中で await を使用できる。
ミューテックス( System.Threading.Mutex )による排他制御
ミューテックスの概念に関してはリンク先を参照のこと。
Semaphore と同様、名前つきにすることで他のプロセスと共有されるシステムミューテックスを作成できる。
まとめ
下表におけるリソース消費量はあくまでも比較である。lock が最もリソース効率が良く、パフォーマンス面で有利である。
本文中に await を使用する場合は、SemaphoreSlim を、他プロセスとの共有(システムセマフォ)を行う場合は Semaphore を用いると良い。
Mutex は Semaphore よりもリソース効率が悪く、積極的に用いる利点は薄い。
(これは Mutex の実装が古く、.NET向けに最適化されていないことに起因する。)
比較の詳細は出典元を参照のこと。
|
lock |
SemaphoreSlim |
Semaphore |
Mutex |
同時エントリ数 |
1 |
可変 |
可変 |
1 |
await の使用 |
不可 |
可 |
不可 |
不可 |
他プロセスと共有 |
不可 |
不可 |
可 |
可 |
リソース消費 |
最小 |
小 |
大 |
最大 |