lockでは非同期処理を扱えない
C# には、共有資源に対する排他制御をするために lock
ステートメントが用意されています。次のように使います。
private object lockObject = new object();
private Task SomeWorkAsync() => Task.Delay(1000); // 適当な非同期処理
private void DoWork()
{
lock(lockObject)
{
// ここの処理は複数のスレッドが同時に侵入することはない
// クリティカルセクション
// await SomeWorkAsync(); // awaitできない!!
// lockステートメントの本体で待機することはできません。
}
}
この例では、lockObject
に対してロックをかけることで、以下のブロック内の処理をシングルスレッドのように扱うことができるようになっています。また、lock中に別のスレッドから処理が呼び出された場合、ロックされているオブジェクトが解放されるまで(つまりブロックから処理が抜けるまで)、待機状態となります。
lock
ステートメントを使った排他制御はとてもお手軽でわかりやすいのですが、lock
ステートメントの本体で await
キーワードを使用することはできません。つまり排他制御をしつつ非同期処理をうまく扱うには別の方法が必要となります。
Semephore, SemaphoreSlim クラスを使う
セマフォによる共有資源の排他制御だと、明示的に自分でロックを解放する必要がありますが、非同期処理でも待機可能です。基本的には try-finally
で待機/ロックと解放を実装します。
// 初期状態でアクセスできるカウント1, 最大アクセス可能数1で初期化
private Semaphore Semaphore = new Semaphore(1, 1);
private async void DoWorkAsync1()
{
// アクセスできるまで待機
// アクセスできたらセマフォのカウンタが-1される
Semaphore.WaitOne();
try
{
// ここの処理は複数のスレッドが指定数以上同時に侵入することはない
await SomeWorkAsync(); // 適当な処理を待機したりできる
}
finally
{
// 解放してカウンタを+1する
Semaphore.Release();
}
}
WaitOne
メソッドで待機して Release
メソッドで開放です。例外時でも必ず解放しなければいつまでたっても待機してしまいます。
また、軽量化された SemaphoreSlim
クラスがあります。軽量化されているのでいくつかの機能が省略されて処理が高速化されているようです。そもそも排他制御のためのロックというのはそれなりにコストがかかる処理です。
以下に使い方を示します。
// 初期状態でアクセスできるカウント1, 最大アクセス可能数1で初期化
private SemaphoreSlim SemaphoreSlim = new SemaphoreSlim(1, 1);
private Task SomeWorkAsync() => Task.Delay(1000); // 適当な非同期処理
private async void DoWorkAsync2()
{
// アクセスできるまで待機
// アクセスできたらセマフォのカウンタが-1される
// SemaphoreSlim.Wait(); // こうも書ける
await SemaphoreSlim.WaitAsync();
try
{
// ここの処理は複数のスレッドが指定数以上同時に侵入することはない
await SomeWorkAsync(); // 適当な処理を待機したりできる
}
finally
{
// 解放してカウンタを+1する
SemaphoreSlim.Release();
}
}
await
でロックを待機することができるのも Semaphore
クラスとの違いです。スリムが使えるならこちらのほうが軽量化されているのでいいのかもしれません。
lockで済むならlockを使ったほうが、Semaphore
クラスを使うよりコストが少なく済むので高速です。ご利用は計画的にどうぞ。
以上。
コメントを書く