非同期:awaitを含むコードをロックするには?(AsyncLock編)[C#、VB].NET TIPS

lock/SyncLockステートメントの代わりにAsyncLockクラスを使用して、await/Await演算子を含むコードで排他ロックを行う方法を解説する。

» 2014年11月18日 17時53分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Platform Development]
.NET TIPS
Insider.NET

 

「.NET TIPS」のインデックス

連載目次

対象:.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#、下:VB)
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

AsyncLockクラスのコード(上:C#、下:VB)
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クラスを使って排他ロックするコード例(上:C#、下:VB)
あらかじめ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]


「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。