C# バックグラウンドで常駐するクラスを作ってみた

# はじめに
自分用に System.Threading.Timer をラップして、バックグラウンドで常駐するワーカークラスを作ってみました。
フレームワークは Microsoft .NET 9.0 です。

# ソース
コンストラクタでタイマーを開始し、キャンセルトークンで停止要求を受信します。
停止処理が実際に行われたかどうかは、Disposed プロパティで確認できます。

```ThreadTimer.cs
using Timer = System.Threading.Timer;

namespace TestApp
{
    public class ThreadTimer
    {
        private readonly ILogger _logger;
        private readonly TimeSpan _interval;
        private readonly Action _callbackAction;
        private readonly CancellationToken _token;
        private readonly ManualResetEventSlim _disposedEvent;
        private readonly Timer _timer;

        public WaitHandle Disposed => _disposedEvent.WaitHandle; // 破棄完了の確認用

        public ThreadTimer(
            ILogger logger,
            TimeSpan interval,
            Action callbackAction,
            CancellationToken token)
        {
            _logger = logger;
            _interval = interval;
            _callbackAction = callbackAction;
            _token = token;
            _disposedEvent = new(false);

            // 0.1秒待ってタイマーを開始する
            TimeSpan dueTime = TimeSpan.FromMilliseconds(100);
            _timer = new Timer(TimerCallback, null, dueTime, _interval);
        }

        private void TimerCallback(object? state)
        {
            try
            {
                // 再入防止のため、タイマーを停止する
                _timer.Change(Timeout.Infinite, Timeout.Infinite);

                // キャンセルのリクエストがあった場合は
                // 処理を中断する
                if (_token.IsCancellationRequested)
                {
                    return;
                }

                // コールバック関数の実行
                _callbackAction();
            }
            catch (Exception ex)
            {
                _logger.Exception(ex);
            }
            finally
            {
                if (_token.IsCancellationRequested)
                {
                    // キャンセルのリクエストがあった場合は
                    // タイマーを破棄する
                    _timer.Dispose();

                    // 破棄完了を通知する
                    _disposedEvent.Set();
                }
                else
                {
                    // キャンセルされていない場合
                    // タイマーを再開する
                    _timer.Change(_interval, _interval);
                }
            }
        }
    }
}
```
# 使い方
WinForms での例です。
開始 / 終了ボタンでスレッドの生成と破棄を行います。

```MainForm.cs

// 開始ボタンクリック
private void BtnStart_Click(object sender, EventArgs e)
{
    // 画面制御
    StartButton.Enabled = false;
    FooterStatus.Text = "開始準備中...";
    FooterProgressBar.Visible = true;
            
    // トークンの作成
    _cancellationTokenSource = new();
    _token = _cancellationTokenSource.Token;

    // スレッドの開始
    _timer1 = new(
        new Logger("_timer1"),
        TimeSpan.FromMilliseconds(1000),
        () => OnTestMethod1(),
        _token);

    _timer2 = new(
        new Logger("_timer2"),
        TimeSpan.FromMilliseconds(500),
        () => OnTestMethod2(),
        _token);

    _timer3 = new(
        new Logger("_timer3"),
        TimeSpan.FromMilliseconds(5000),
        () => OnTestMethod3(),
        _token);
        
    // 画面制御
    FooterProgressBar.Visible = false;
    FooterStatus.Text = "動作中";
    StopButton.Enabled = true;
}

// 停止ボタンクリック
private async void BtnStop_Click(object sender, EventArgs e)
{
    // 画面制御
    StopButton.Enabled = false;
    FooterStatus.Text = "停止準備中...";
    FooterProgressBar.Visible = true;

    // キャンセルの要請
    _cancellationTokenSource.Cancel();

    // タイマーの終了を待機する
    await Task.WhenAll(
        Task.Run(() => { _timer1.Disposed.WaitOne(); }),
        Task.Run(() => { _timer2.Disposed.WaitOne(); }),
        Task.Run(() => { _timer3.Disposed.WaitOne(); })
    );

    // 画面制御
    FooterProgressBar.Visible = false;
    FooterStatus.Text = "停止完了";
    StartButton.Enabled = true;
}
```

# おわりに
お仕事では bool と lock を使ってゴリゴリに制御しているソースを見ることが多いので、今回 CancellationToken や ManualResetEvent を勉強できてよかったです。