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 非同期だけど同時に実行してほしくない処理();
}
}