MRが楽しい

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

C#でSemaphoreSlimを使ってスレッド制御を行う その2(待機キューを1つまでに制限する)

本日はC#の小ネタ枠です。
C#でSemaphoreSlimを使ってスレッド制御を行う方法についてです。

前回記事

以下の前回記事の続きです。
bluebirdofoz.hatenablog.com

今回はSemaphoreSlimを使ったラッパークラスを作成することで待機キューは1つまでとし、2つ目以降は即キャンセルする制御を行ってみました。

ラッパークラス

SemaphoreSlimを使った以下のラッパークラスを作成しました。
TryEnterAsyncメソッドでセマフォの取得を行い、1つまでは指定時間の待機を許可します。
・LimitedSemaphore.cs

using System.Threading;
using System.Threading.Tasks;

public class LimitedSemaphore
{
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
    private int _waitingCount = 0; // 待機中のタスク数

    /// <summary>
    /// セマフォの取得
    /// </summary>
    /// <param name="timeoutMs">待機タイムアウト(ミリ秒)</param>
    /// <param name="cancellationToken">キャンセルトークン</param>
    public async Task<bool> TryEnterAsync(int timeoutMs = 0, CancellationToken cancellationToken = default)
    {
        // まず即取得を試みる
        if (await _semaphore.WaitAsync(0, cancellationToken))
        {
            return true;
        }

        // 実行中のタスクがある場合、待機タスクが1つまで許可
        if (Interlocked.CompareExchange(ref _waitingCount, 1, 0) != 0)
        {
            return false; // 既に待機タスクがいる場合はキャンセル
        }

        try
        {
            // 待機タスクとしてセマフォ取得を試みる
            if (await _semaphore.WaitAsync(timeoutMs, cancellationToken))
            {
                return true;
            }
            return false;
        }
        finally
        {
            // 待機タスクが完了したらカウントを減らす
            Interlocked.Exchange(ref _waitingCount, 0);
        }
    }

    /// <summary>
    /// セマフォの解放
    /// </summary>
    public void Release()
    {
        _semaphore.Release();
    }
}

動作確認のため、Unityで以下のテストスクリプトを作成しました。
・ScopedFlagTest.cs

using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public class SemaphoreSlimTest2 : MonoBehaviour
{
    private LimitedSemaphore limitedSemaphore = new LimitedSemaphore();

    public async void ButtonEvent()
    {
        // スレッドをブロックする(待機中のタスクがあればスキップ)
        if (!await limitedSemaphore.TryEnterAsync(100000))
        {
            Debug.Log($"{name}: 他のタスクが待機中なのでスキップ");
            return;
        }

        // 1秒ごとに1~3のカウントダウンをログに出力する
        for (int count = 1; count <= 3; count++)
        {
            Debug.Log(count);
            await Task.Delay(1000);
        }

        // スレッドを解放する
        limitedSemaphore.Release();
    }
}

処理を実行してみます。
前回記事と違い、複数回ボタンを押下すると2つ目以降の待機は即キャンセルされるようになりました。