C#で定期的な処理を実行するという要件は、アプリケーション開発において非常に頻繁に登場します。
かつては複数のタイマークラスが用途別に存在しており、どれを選択すべきか迷う場面も少なくありませんでした。
しかし、モダンな.NET環境においては、PeriodicTimerの登場によってその最適解が明確になりました。
本記事では、2026年現在のスタンダードとなっているPeriodicTimerの活用方法と、なぜこのクラスが推奨されるのかという理由を詳しく解説します。
PeriodicTimerの登場背景と従来のタイマーとの比較
C#の歴史の中で、タイマーの実装にはいくつかの変遷がありました。
初期の.NETから存在するタイマーは、主にコールバック方式を採用していました。
コールバック方式は、指定した時間が経過するたびに特定のメソッドを呼び出す仕組みです。
しかし、この方式は非同期処理(async/await)との相性が決して良いとは言えませんでした。
コールバック内で非同期メソッドを呼び出すと、状態管理が複雑になり、デッドロックやリソースリークの原因となることもありました。
こうした課題を解決するために導入されたのが、PeriodicTimerという新しい選択肢です。
PeriodicTimerは、従来のタイマーとは異なり、ループ構造の中で非同期的に待機するスタイルを提供します。
これにより、コードの可読性が飛躍的に向上し、実行フローの制御が極めて容易になりました。
従来の主要なタイマーとPeriodicTimerの違いを以下の表にまとめました。
| タイマーの種類 | 名前空間 | 主な特徴 | 非同期への対応 |
|---|---|---|---|
| System.Threading.Timer | System.Threading | 軽量でスレッドプールを使用する | 低い(コールバック) |
| System.Timers.Timer | System.Timers | イベントベースでサーバーサイド向き | 低い(イベント) |
| PeriodicTimer | System.Threading | ループ構造で待機が可能 | 非常に高い |
PeriodicTimerの基本的な実装方法
PeriodicTimerの最大の特徴は、whileループと組み合わせて使用することにあります。
インスタンスを生成する際に、実行間隔(TimeSpan)を指定します。
その後、WaitForNextTickAsyncメソッドを呼び出すことで、次の実行タイミングまで非同期的に待機します。
まずは、最もシンプルな実装例を見てみましょう。
// 1秒間隔で実行するPeriodicTimerを生成
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
// ループ内で待機と処理を繰り返す
while (await timer.WaitForNextTickAsync())
{
// ここに定期実行したい処理を記述
Console.WriteLine($"実行時刻: {DateTime.Now:HH:mm:ss}");
}
実行時刻: 10:00:01
実行時刻: 10:00:02
実行時刻: 10:00:03
このコードの注目すべき点は、コールバック関数を必要としないことです。
通常の順次処理と同じようにコードを記述できるため、文脈(コンテキスト)の維持が非常に簡単です。
また、タイマー自体がIDisposableを実装しているため、usingステートメントによる適切なリソース解放が可能です。
非同期処理におけるPeriodicTimerの優位性
モダンな開発において、定期実行される処理そのものが非同期(async)であるケースは多々あります。
従来のタイマーでは、コールバックが重なった場合の制御(リエントラント)が困難でした。
例えば、処理時間がタイマーの間隔を超えてしまった場合、複数のスレッドで同時に処理が走ってしまうリスクがあります。
PeriodicTimerを使用すると、この問題は自然に解決されます。
なぜなら、await timer.WaitForNextTickAsync()は、前の処理が完了した後に次の待機を開始するためです。
つまり、デフォルトで多重実行を防止する構造になっています。
この「自己調整」の性質は、データベースのポーリングやAPIの定期監視において極めて安全に機能します。
キャンセレーションへの対応
実務においては、アプリケーションの終了時やユーザーの操作によって、タイマーを停止させる必要があります。
PeriodicTimerは、CancellationTokenをネイティブにサポートしています。
WaitForNextTickAsyncにトークンを渡すことで、安全かつ即座にループを抜けることができます。
public async Task RunBackgroundJob(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
try
{
// キャンセルトークンを渡して待機
while (await timer.WaitForNextTickAsync(ct))
{
await ProcessDataAsync();
}
}
catch (OperationCanceledException)
{
// キャンセル時はここを通る
Console.WriteLine("タイマーが停止しました。");
}
}
この実装により、CancellationTokenSource.Cancel()が呼ばれた際に、無駄な待機をせずに処理を終了できます。
これは、クラウドネイティブな環境でのサービス停止や、ポッドの再起動時におけるクリーンアップ処理に欠かせません。
実行間隔の動的変更(.NET 8以降の進化)
以前のPeriodicTimerは、一度設定した間隔を途中で変更することができませんでした。
しかし、.NET 8以降ではPeriodプロパティが更新可能になり、さらに柔軟性が増しました。
これにより、システムの負荷状況に応じてポーリングの間隔を調整するといった高度な制御が可能です。
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync())
{
if (IsHighLoad())
{
// 負荷が高い場合は間隔を10秒に広げる
timer.Period = TimeSpan.FromSeconds(10);
}
else
{
// 通常時は5秒に戻す
timer.Period = TimeSpan.FromSeconds(5);
}
await DoWorkAsync();
}
このように、実行中にタイマーの挙動をカスタマイズできる点は、複雑なビジネスロジックを実装する上で大きな武器となります。
最適な活用シーン
PeriodicTimerは汎用的ですが、特に適したシーンがいくつか存在します。
第一に、ASP.NET CoreのBackgroundService(ワーカーサービス)での利用です。
サーバーサイドでのバッチ処理や、キャッシュの定期更新にはPeriodicTimerが最適です。
第二に、IoTデバイスからのデータ収集です。
センサーの値を一定間隔で読み取り、クラウドに送信するようなケースでは、非同期I/Oと組み合わせたPeriodicTimerが効率的です。
第三に、UIの状態監視です。
MVVMパターンなどでバックグラウンドからViewModelの値を定期的に更新したい場合にも、スレッド制御のしやすさから重宝されます。
実装上の注意点とエラーハンドリング
PeriodicTimerを使用する際に、必ず意識すべき注意点があります。
それは、ループ内での例外が原因でタイマーが停止してしまうという問題です。
もしwhileループ内の処理でキャッチされない例外が発生すると、ループ自体が終了してしまいます。
永続的に稼働させたいタイマーの場合は、ループ内をtry-catchで囲むことが必須となります。
while (await timer.WaitForNextTickAsync())
{
try
{
await CriticalTaskAsync();
}
catch (Exception ex)
{
// 例外をログに記録し、ループは継続させる
LogError(ex);
}
}
また、PeriodicTimerは「厳密なリアルタイム性」を保証するものではありません。
OSのスケジューリングやガベージコレクション(GC)の影響により、数ミリ秒程度の誤差が生じる可能性があります。
厳密な精度が求められる計測器制御などでは、ハードウェアタイマーの検討が必要になる場合もありますが、一般的なビジネスアプリケーションであればPeriodicTimerの精度で十分です。
パフォーマンスへの影響
PeriodicTimerは、メモリ効率の面でも優れています。
従来のタイマーはコールバックごとに新しいスレッドを割り当てたり、複雑な同期オブジェクトを生成したりすることがありました。
一方、PeriodicTimerはValueTaskを活用した非同期設計となっており、不必要なアロケーション(メモリ確保)を最小限に抑えています。
これにより、大量のタイマーを同時に動かすようなスケーラビリティが求められる環境でも、GCの負荷を低減させることが可能です。
また、待機中にはスレッドを占有しない(ブロックしない)ため、システム全体の同時実行性能を損なうことがありません。
まとめ
PeriodicTimerは、現代のC#開発において定期実行処理を実装するための「標準ツール」と言える存在です。
非同期処理との親和性、多重実行の自然な防止、そしてキャンセレーションの容易な統合といった多くの利点があります。
従来のTimerクラスを使用している既存のコードも、メンテナンスの機会があればPeriodicTimerへの移行を検討する価値が十分にあります。
特に、async/awaitを多用する現代のアプリケーションにおいては、PeriodicTimer以外の選択肢は考えにくいと言っても過言ではありません。
正しいエラーハンドリングとリソース管理を組み合わせることで、堅牢で効率的なバックグラウンドタスクを構築しましょう。
今回紹介したテクニックを活用し、ぜひあなたのプロジェクトでもモダンな定期実行処理を実現してください。
