[C#] 非同期async/awaitを含むメソッドでlockする方法

[C#] 非同期async/awaitを含むメソッドでlockする方法

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 クラスを使うよりコストが少なく済むので高速です。ご利用は計画的にどうぞ。

以上。

参考URL

C#カテゴリの最新記事