C# では,非同期なメソッドでは lock が使えません.この記事ではそれでも lock したいときはどうするのっていうお話をします.
たとえば,こんなふうにダブルチェックロッキングしたいとしますね.
// これが複数のスレッドから非同期に呼ばれるprivatestaticasyncValueTask条件満たしたらなんかするAsync(){if(条件){lock(_lockObj){if(条件){awaitなんかすごく重いIOバウンドなやつAsync();}}}}
lock ステートメントは非同期メソッド内で使えないので,実際にはこのコードはコンパイルが通りません.
じゃあどうしようか,といって,一番ラクな逃げ道は同期にしちゃうことです.カンタンカンタン.
// これが複数のスレッドから非同期に呼ばれるprivatestaticvoid条件満たしたらなんかする(){if(条件){lock(_lockObj){if(条件){なんかすごく重いIOバウンドなやつAsync().Wait();}}}}
Task だって Wait しちゃえばただの同期,これはちゃんと動きます.でもここで「いやなんのための async なんだよ」ってなりますよね. lock したいだけなのに,そのために非同期の恩恵を捨て去る,そんなことやっちゃダメです.
じゃあ最新技術をこね回して難しいコード書くのかっていうとそんなことはなく,むしろ全く逆で古くからある技術を使います.そう,セマフォです.
セマフォっていうと OS の機能で,プロセス間の資源のアクセス制御に使うイメージですが,.NET にはプロセス内で利用するための SemaphoreSlim
クラスがあります.
セマフォといっても結局待たなきゃいけないでしょって話なんですが, SemaphoreSlim
クラスには非同期で待てる WaitAsync()
メソッドがあるわけです.
これをつかうと完全に非同期な lock が実現できるわけですね.
これはもうパターンが固定なので,ちょっと汎用的に AsyncLock
なんていうクラスを作っておくとどこでも使えます.
やってることもとてもカンタンなので,作り方さえ覚えてしまえばとっさのときにも書けます.
/// <summary>/// async な文脈での lock を提供します./// Lock 開放のために,必ず処理の完了後に LockAsync が生成した IDisposable を Dispose してください./// </summary>publicsealedclassAsyncLock{privatereadonlySemaphoreSlim_semaphore=newSemaphoreSlim(1,1);publicasyncTask<IDisposable>LockAsync(){await_semaphore.WaitAsync();returnnewHandler(_semaphore);}privatesealedclassHandler:IDisposable{privatereadonlySemaphoreSlim_semaphore;privatebool_disposed=false;publicHandler(SemaphoreSlimsemaphore){_semaphore=semaphore;}publicvoidDispose(){if(!_disposed){_semaphore.Release();_disposed=true;}}}}
面倒な人はこれをコピペでいいです.使うときは, lock 構文の代わりに using 構文を使います.これによって, semaphore の管理を忘れて lock という意味を持たせた見た目のコードを書けます.
たとえば,最初の例を書いてみるとこんな感じです.
privatestaticreadonlys_lock=newAsyncLock();privatestaticasyncTask条件満たしたらなんかするAsync(){if(条件){using(awaits_lock.LockAsync()){if(条件){awaitなんかすごく重いIOバウンドなやつAsync();}}}}
lock が using に変わっただけで,あとはあまり変わりません.でもこれはコンパイルも通るし,ちゃんと非同期でパフォーマンスよく動きます.
というわけで,いままで .Wait
しちゃってた方,今日からは await
しましょう!