awaitを使った時のロックって、どうやるんですかね。セマフォでいいの?
C#では、バージョン5.0から、async/awaitキーワードを用いることで、非同期処理がとてつもなく簡単に書けるようになりました。
今までは、時間のかかる処理は新しいスレッドを作ったり、Taskで実行したり、IO処理はBegin/Endパターンとか、EAP使ったりしました。うんざりしますね!まじで!!
今では、async/awaitが導入されたことにより、ほぼ同期的なコードと同じコードを書いてるにもかかわらず、時間のかかる処理やIO処理を非同期に行えるようになりました。
さて、同期的にコードが書けてしまうと、おもわず非同期部分をlockしたくなってしまいます。
たとえばファイルの書き込みとか。非同期にしたいけど、書き込み自体は排他的にしてほしい。
lock(lockObj) { using (var stream = File.OpenWrite(path)) { await stream.WriteLineAsync("A"); await stream.WriteLineAsync("B"); } }
みたいな。でもこれはアウトです。
lockの実態はMonitorですが、これはあるスレッドでロックして、別スレッドで返すことはできません。
await前後は同じスレッドで実行されるとは限らないため、lock内にawaitを入れるとエラーが出るわけです。
で…
どうすればいいの?これ。
とりあえず私は、SemaphoreSlimを使っています。普通のSemaphoreでもいいんですが、Slimのほうにはシグナルを待つためにWaitAsyncというTask<bool>を返すメソッドがあって、非同期処理と相性が良いようにも思えます。こんな感じ↓
private SemaphoreSlim _semaphore = new SemaphoreSlim(1); private async Task FunctionAsync() { try { await _semaphore.WaitAsync(); await 非同期だけど同時に実行してほしくない処理(); } finally { _semaphore.Release(); } }
で、毎回try/finally書くのはめんどくさいんで、こんな↓クラス作って
using System; using System.Threading; using System.Threading.Tasks; namespace ShComp { /// <summary> /// セマフォを用いた、非同期の状態でもロックを行うためのメソッドを提供します。 /// </summary> class AsyncLock { private SemaphoreSlim _s; public AsyncLock() { _s = new SemaphoreSlim(1); } /// <summary> /// <see cref="System.Threading.SemaphoreSlim" /> に移行するために非同期に待機します。<para /> /// 待機後、IDisposable型のオブジェクトを返します。 /// 返されるオブジェクトをDisposeすることで <see cref="System.Threading.SemaphoreSlim" /> から出ます。 /// usingと共に使うことを想定しています。 /// </summary> public async Task<IDisposable> GetObjectAsync() { await _s.WaitAsync(); return new _(_s); } private class _ : IDisposable { private SemaphoreSlim _s; public _(SemaphoreSlim s) { _s = s; } public void Dispose() { _s.Release(); } } } }
こんな感じ↓で使っています。 …いいのかな?
private AsyncLock _lock = new AsyncLock(); private async Task FunctionAsync() { using (await _lock.GetObjectAsync()) { await 非同期だけど同時に実行してほしくない処理(); } }