非同期:awaitを含むコードをロックするには?(AsyncLock編)[C#、VB]:.NET TIPS
lock/SyncLockステートメントの代わりにAsyncLockクラスを使用して、await/Await演算子を含むコードで排他ロックを行う方法を解説する。
対象:.NET 4.5以降
async修飾子/await演算子(VBではAsync修飾子/Await演算子、以降では省略)を使った非同期プログラミングでは、スレッド間の排他ロックにlockステートメント(VBではSyncLockステートメント)が使えない。代わりにSemaphoreSlimクラス(System.Threading名前空間)を使えば可能なのだが、コードの記述が面倒である。そこで本稿では、AsyncLockクラスを使って簡潔に記述する方法を説明する。
SemaphoreSlimクラスによる排他ロック
SemaphoreSlimクラスを使ってスレッド間の排他ロックを行うコードは、次のようなものだ。lockステートメントが使えない理由も含めて、詳細は「非同期:awaitを含むコードをロックするには?(SemaphoreSlim編)[C#、VB]」を参照していただきたい。
static System.Threading.SemaphoreSlim _semaphore
= new System.Threading.SemaphoreSlim(1, 1);
static async Task LongTimeMethod2Async(string id)
{
await _semaphore.WaitAsync(); // ロックを取得する
try
{
Console.WriteLine("{0} - ({1}) START ", DateTime.Now.ToString("ss.fff"), id);
await Task.Delay(1000); // この行は別スレッドで実行される
// これ以降は、元と同じスレッドで実行されるとは限らない
Console.WriteLine("{0} - ({1}) END", DateTime.Now.ToString("ss.fff"), id);
}
finally
{
_semaphore.Release(); // 違うスレッドでロックを解放してもOK
}
}
Private _semaphore As System.Threading.SemaphoreSlim _
= New System.Threading.SemaphoreSlim(1, 1)
Private Async Function LongTimeMethod2Async(id As String) As task
Await _semaphore.WaitAsync() ' ロックを取得する
Try
Console.WriteLine("{0} - ({1}) START ", DateTime.Now.ToString("ss.fff"), id)
Await Task.Delay(1000) ' この行は別スレッドで実行される
' これ以降は、元と同じスレッドで実行されるとは限らない
Console.WriteLine("{0} - ({1}) END", DateTime.Now.ToString("ss.fff"), id)
Finally
_semaphore.Release() ' 違うスレッドで解放してもOK
End Try
End Function
SemaphoreSlimクラスはスレッドアフィニティがないので、ロック中にスレッドが変わっても問題ない。
なお、C#ではSystem名前空間とSystem.Threading.Tasks名前空間をインポートしておく必要がある(以下同じ)。
上のコードは、いちいちtry〜finallyステートメント(VBではTry〜Finallyステートメント)で囲まなければならず、コーディングが面倒である。lock/SyncLockステートメントのように簡潔に書けないだろうか?
AsyncLockクラスで排他ロックを簡潔に記述するには?
SemaphoreSlimクラスをラップしてlock/SyncLockステートメントのようなコーディングを可能にするのが、AsyncLockクラスだ。AsyncLockクラスは、マイクロソフトの「Parallel Programming with .NET」ブログで発表されたコードである。そして、発表時にはこま切れのコード断片だったものを、Scott Hanselman氏がまとめて「Comparing two techniques in .NET Asynchronous Coordination Primitives」に掲載してくれた。そのAsyncLockクラスのコードは次の通りである。
public sealed class AsyncLock
{
private readonly System.Threading.SemaphoreSlim m_semaphore
= new System.Threading.SemaphoreSlim(1, 1);
private readonly Task<IDisposable> m_releaser;
public AsyncLock()
{
m_releaser = Task.FromResult((IDisposable)new Releaser(this));
}
public Task<IDisposable> LockAsync()
{
var wait = m_semaphore.WaitAsync();
return wait.IsCompleted ?
m_releaser :
wait.ContinueWith(
(_, state) => (IDisposable)state,
m_releaser.Result,
System.Threading.CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default
);
}
private sealed class Releaser : IDisposable
{
private readonly AsyncLock m_toRelease;
internal Releaser(AsyncLock toRelease) { m_toRelease = toRelease; }
public void Dispose() { m_toRelease.m_semaphore.Release(); }
}
}
Public Class AsyncLock
Private ReadOnly m_semaphore As System.Threading.SemaphoreSlim _
= New System.Threading.SemaphoreSlim(1, 1)
Private ReadOnly m_releaser As Task(Of IDisposable)
Public Sub New()
m_releaser = Task.FromResult(CType(New Releaser(Me), IDisposable))
End Sub
Public Function LockAsync() As Task(Of IDisposable)
Dim wait = m_semaphore.WaitAsync()
Return If(wait.IsCompleted,
m_releaser,
wait.ContinueWith(
Function(t, state) CType(state, IDisposable),
m_releaser.Result,
System.Threading.CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default
))
End Function
Private NotInheritable Class Releaser
Implements IDisposable
Private ReadOnly m_toRelease As AsyncLock
Friend Sub New(toRelease As AsyncLock)
m_toRelease = toRelease
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
m_toRelease.m_semaphore.Release()
End Sub
End Class
End Class
C#のコードは「Comparing two techniques in .NET Asynchronous Coordination Primitives」より。
VBのコードは、C#のコードから筆者が翻訳した。
AsyncLockクラスは、コンストラクト時にSemaphoreSlimクラスのオブジェクトを生成してメンバー変数に保持し、LockAsyncメソッドでそのSemaphoreSlimオブジェクトをロックする。また、LockAsyncメソッドが返したオブジェクトのDisposeメソッドが呼び出されたときに、SemaphoreSlimオブジェクトに対するロックを解放するようになっている。
このAsyncLockクラスを使って冒頭のコードを書き直すと、次のようになる。
static AsyncLock _asyncLock = new AsyncLock();
static async Task LongTimeMethod2Async(string id)
{
using (await _asyncLock.LockAsync())
{
Console.WriteLine("{0} - ({1}) START ", DateTime.Now.ToString("ss.fff"), id);
await Task.Delay(1000);
Console.WriteLine("{0} - ({1}) END", DateTime.Now.ToString("ss.fff"), id);
}
}
Private _asyncLock As AsyncLock = New AsyncLock()
Private Async Function LongTimeMethod2Async(id As String) As task
Using Await _asyncLock.LockAsync()
Console.WriteLine("{0} - ({1}) START ", DateTime.Now.ToString("ss.fff"), id)
Await Task.Delay(1000)
Console.WriteLine("{0} - ({1}) END", DateTime.Now.ToString("ss.fff"), id)
End Using
End Function
あらかじめAsyncLockクラスのインスタンスを生成しておく。あとは、排他ロックしたい部分で、AsyncLockオブジェクトのLockAsyncメソッドを呼び出してusing/Usingステートメントで囲む。すると、LockAsyncメソッドを呼び出したところで排他ロックが掛かり、using/Usingステートメントから抜けるときにそのロックが解放される。
try〜finallyステートメント(VBではTry〜Finallyステートメント)がusing/Usingステートメントに変わり、すっきりしたコードになった。
利用可能バージョン:.NET Framework 4.5以降
カテゴリ:クラスライブラリ 処理対象:非同期処理
使用ライブラリ:AsyncLockクラス(マイクロソフト「Parallel Programming with .NET」ブログ)
関連TIPS:非同期:awaitを含むコードをロックするには?(SemaphoreSlim編)[C#、VB]
Copyright© Digital Advantage Corp. All Rights Reserved.