[C#] awaitを含むコードの排他制御

# この投稿の前提情報

`await`を含むということはほとんどの場合時間的に長い処理であり、この間ずっと他のスレッドをブロックしっぱなしというものは決して褒められたコードではない。
できるのであれば、その非同期メソッド内の同期処理部分の必要な部分だけを`lock`するのが最善である。
ここに記載されている情報は、嬉々として採用するべきものではなく、

- 非同期部分を同時に呼び出してしまうと困ったことがおきる
- 非同期メソッド呼び出し前後でデータの整合性を保つ必要がある

といった状況化で仕方なく、しぶしぶ採用するべきものになる。

# `lock`ブロック内では`await`できない

通常、排他制御をおこなう場合、`lock`ステートメントを使用する。

```csharp
readonly object LockHandler = new object();

void Hoge()
{
    lock (LockHandler)
    {
        DoSomething();
    }
}
```

しかし、この`lock`ブロック内に`await`が含まれるコードはコンパイルエラーが発生する。
というのも、ロックを解放するのはそれを獲得したスレッドでなければならず、`await`演算子の前と後ろは違うスレッドで実行される可能性があるためだ。

```csharp
// Compile Error
lock (LockHandler)
{
    await DoSomethingAsync();
}
```

# `SempahoreSlim`を使う

そこで、ロックの獲得と解放を異なるスレッドで行うことのできる仕掛けが必要になる。
これに適したクラスが`SemaphoreSlim`であり、これで`await`を含むコードの排他処理ができるようになる。

```csharp
readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1);

public async Task AsynchronousMethodAsync()
{
    await Semaphore.WaitAsync();
    try
    {
        await DoSomethingAsync();
    }
    finally
    {
        Semaphore.Release();
    }
}
```

もし、同じクラスの同期処理部分でも排他制御をおこないたければ、同じ`SemaphoreSlim`オブジェクトの`Wait`メソッドを使用すればよい。

```csharp
public void SynchronousMethod()
{
    Semaphore.Wait();
    try
    {
        DoSomething();
    }
    finally
    {
        Semaphore.Release();
    }
}
```

# `SemaphoreSlim`の注意点

`SemaphoreSlim`を使用している場合、`lock`ステートメントと異なり、すでに獲得しているロックを再取得できない。
以下のコードは1を出力して、そのまま停止する。2には到達しない。

```csharp
readonly SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1);

public async Task AsynchronousMethodAsync()
{
    await Semaphore.WaitAsync();
    try
    {
        Console.WriteLine("1");          // 1
        await Semaphore.WaitAsync();
        Console.WriteLine("2");          // 2
    }
    finally
    {
        Semaphore.Release();
    }
}
```

これが意味するのは、ある`SemaphoreSlim`オブジェクトでガードされているメソッドから、同じオブジェクトでガードされているメソッドを呼び出せないということだ。
以下の`AsynchronousMethodAsync()`は、`SynchronousMethod()`の`Semaphore.Wait()`で処理が停止し、それ以上処理が継続されない。

```csharp
public async Task AsynchronousMethodAsync()
{
    await Semaphore.WaitAsync();
    try
    {
        SynchronousMethod();
        await ...
    }
    finally
    {
        Semaphore.Release();
    }
}

public void SynchronousMethod()
{
    Semaphore.Wait();
    ...
}
```


# `IDisposable`を実装するべきか

`SemaphoreSlim`は`IDisposable`を実装している。そして、この型をインスタンスフィールドに含むクラスは一般論で言えば`IDisposable`を実装し、そこで`SepamhoreSlim`オブジェクトを`Dispose`するべきだということになる。
つまり、排他制御しているオブジェクトを使いおわったら`Dispose()`する必要がある。`lock`ステートメントで済んでいたクラスと比べて、とても面倒だ。

安心して欲しい。`SemaphoreSlim`のソースを追えばわかることだが、`AvailableWaitHandle`プロパティを使用しない場合、その`Dispose()`は内部の変数にnullを代入する以外の仕事はおこなわれない。
そして、今回の要件ではこのプロパティは不要なので、`SemaphoreSlim`オブジェクトを`Dispose`しなくても実質的な問題は生じない。

もちろん未来永劫そうであると保証があるわけではない。
どうするべきかという問いには、「`IDisposable`を実装するべきだ」と回答するしかない。
だから、コードレビューで原理主義者に追求を受けそうだというのなら、`IDisposable`を実装することになるだろう。

現実的なところでは、`SemaphoreSlim`のラッパークラスを作り、`AvailableWaitHandle`を呼び出せなくしてしまうという手がある。
うるさいコード分析ツールを黙らせるのも楽になるだろう。コンストラクタの引数`(1, 1)`を隠蔽できるようになるのもいい。

`AvailableWaitHandle`のない`SemaphoreSlim`を再実装してしまう方法もある。
何かまちがっている気もするが、こわいコードレビュアーをかかえたチームでは歓迎されるのではないだろうか。

# `AsyncLock`とその注意点

https://www.atmarkit.co.jp/ait/articles/1411/18/news135.html にあるように、`SemaphoreSlim`そのものを使うのではなく、それを使いやすくしたクラスを使用するという方法がある。

```csharp
readonly AsyncLock LockHandler = new AsyncLock();

public async Task AsynchronousMethodAsync()
{
    using (await LockHandler.LockAsync())
    {
        await DoSomethingAsync();
    }
}
```

簡潔で美しい方法だが、`await`を忘れ`using (LockHandler.LockAsync())`としてしまうと、ロックされている場合に実行中の`Task`を`Dispose`しようとして例外を投げるコードができあがってしまう。
困ったことに、非同期メソッドの呼び出しが`await`されてないという警告も出力されない。

若干一名の尊い犠牲のもとになりたっている情報なので、もし採用するのなら注意してほしい。
犠牲担当の意見としては、二度と利用する気はない。

# 参考にした情報
非同期：awaitを含むコードをロックするには？（SemaphoreSlim編）［C#、VB］
https://www.atmarkit.co.jp/ait/articles/1411/11/news117.html

非同期：awaitを含むコードをロックするには？（AsyncLock編）［C#、VB］
https://www.atmarkit.co.jp/ait/articles/1411/18/news135.html

`SemaphoreSlim`のソース
https://source.dot.net/#System.Private.CoreLib/SemaphoreSlim.cs