MRが楽しい

MRやVRについて学習したことを書き残す

SemaphoreSlimクラスをラップしてlockステートメントのように利用する

本日は C# の小ネタ枠です。
SemaphoreSlim クラスをラップしてlockステートメントのように利用する方法を試します。

前回記事

SemaphoreSlim の使い方については以下の記事を参照ください。
bluebirdofoz.hatenablog.com
bluebirdofoz.hatenablog.com

SemaphoreSlim クラスをラップしてlockステートメントのように利用する

SemaphoreSlim は非同期処理を排他制御できる利点がある一方、lock ステートメントのように記述できず、必ずリソースの解放を記述する必要があります。
そこで SemaphoreSlim クラスをラップして lock ステートメントのように記述するクラスの一例が下記ページに掲載されています。
www.hanselman.com

以下がその AsyncLock クラスです。
・AsyncLock.cs

using System;
using System.Threading.Tasks;

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(); }
  }
}

実際に前回記事のサンプルスクリプトに組み込んで利用してみます。
以下のように lock ステートメントのようにリソースの解放を記述する必要なく非同期処理の排他制御を記述できます。
・AsyncLockTest.cs

using System.Threading.Tasks;
using UnityEngine;
public class AsyncLockTest : MonoBehaviour
{ 
    // AsyncLock クラスを使用する
    private AsyncLock _asyncLock = new();
    
    public async void ButtonEvent()
    {
        // 排他制御を行いたい部分を以下のコードブロックで記述してスレッドをブロックする
        using (await _asyncLock.LockAsync())
        {
            // 1秒ごとに1~3のカウントダウンをログに出力する
            for (int count = 1; count <= 3; count++)
            {
                Debug.Log(count);
                await Task.Delay(1000);
            }
        }
    }
}

サンプルシーンのコンポーネントを差し替えて動作を確認します。
以下の通り、非同期処理の排他制御ができました。

ただしこちらの記法は using (await _asyncLock.LockAsync()) の部分で await の記述忘れが発生しうるという別の問題もあるとのこと。
qiita.com