はじめに
自分用に 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 を勉強できてよかったです。