C#で一定間隔ごとに処理を実行したい場合、多くの開発者が最初に思い浮かべるのがTimerクラスです。
しかし、C#のクラスライブラリには名前空間の異なる複数のタイマーが存在しており、それぞれ動作するスレッドモデルや精度、用途が大きく異なります。
適切なタイマーを選択しないと、UIフリーズの原因になったり、意図しないマルチスレッドによる競合状態(レースコンディション)を招いたりする可能性があります。
本記事では、C#で利用可能な主要な4つのタイマーに、モダンな非同期処理で推奨されるPeriodicTimerを加えた特性を整理し、具体的なコード例とともに使い分け方を詳しく解説します。
1. C#におけるタイマーの全体像
.NET環境において、定期的な処理を実現するためのクラスは主に4種類あります。
これらは一見似ていますが、「どのスレッドで実行されるか」という点が最大の相違点です。
まずは、それぞれのタイマーが属する名前空間と、主な実行環境を整理しましょう。
| タイマー名 | 名前空間 | 実行スレッド | 主な用途 |
|---|---|---|---|
| System.Threading.Timer | System.Threading | スレッドプール | バックグラウンドでの軽量な処理 |
| System.Timers.Timer | System.Timers | スレッドプール (既定) | サーバーサイドや汎用的なイベント処理 |
| System.Windows.Forms.Timer | System.Windows.Forms | UIスレッド | Windows FormsアプリのUI更新 |
| System.Windows.Threading.DispatcherTimer | System.Windows.Threading | UIスレッド (Dispatcher) | WPFやWinUIのUI更新 |
これらに加え、.NET 6以降では非同期プログラミングに最適化された System.Threading.PeriodicTimer も重要な選択肢となっています。
2. System.Threading.Timer:軽量なスレッドプールタイマー
System.Threading.Timer は、もっとも基本的かつ軽量なタイマーです。
指定した時間が経過すると、スレッドプール上のスレッドでコールバックメソッドが実行されます。
2.1 特徴と注意点
このタイマーは、オーバーヘッドが非常に小さいため、大量のタイマーを同時に動かす必要がある場合に適しています。
しかし、以下の点に注意が必要です。
- コールバックはスレッドプールで実行されるため、UIコントロールに直接アクセスすることはできません。
- コールバック内で重い処理を行うと、スレッドプールを圧迫する可能性があります。
- タイマーのインスタンスがガベージコレクション(GC)によって回収されないよう、参照を保持し続ける必要があります。
2.2 実装例
using System;
using System.Threading;
class Program
{
static void Main()
{
// タイマーの作成
// 第1引数: 実行するメソッド
// 第2引数: メソッドに渡す状態オブジェクト
// 第3引数: 開始までの遅延(ミリ秒)
// 第4引数: 実行間隔(ミリ秒)
var timer = new Timer(TimerCallback, "State Info", 0, 1000);
Console.WriteLine("タイマーを開始しました。エンターキーで終了します。");
Console.ReadLine();
// リソースの解放
timer.Dispose();
}
private static void TimerCallback(object state)
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} - 実行中: {state}");
}
}
タイマーを開始しました。エンターキーで終了します。
15:30:00.001 - 実行中: State Info
15:30:01.005 - 実行中: State Info
15:30:02.002 - 実行中: State Info
3. System.Timers.Timer:サーバーサイド向けの汎用タイマー
System.Timers.Timer は、System.Threading.Timer をラップして使いやすくしたものです。
「Elapsed」イベントを利用して処理を記述できるため、より直感的に実装できます。
3.1 特徴とSynchronizingObject
このタイマーの最大の特徴は、SynchronizingObject プロパティを持っていることです。
ここにフォームやコントロールを指定すると、イベントハンドラがUIスレッドで呼び出されるようになります。
- デフォルトではスレッドプールで実行されます。
AutoResetプロパティにより、一度きりの実行か、繰り返しの実行かを簡単に制御できます。- Windowsフォームのツールボックスにも表示されるコンポーネントベースのタイマーです。
3.2 実装例
using System;
using System.Timers;
class Program
{
static void Main()
{
// インスタンス化
var timer = new System.Timers.Timer(1000);
// イベントハンドラの設定
timer.Elapsed += OnTimedEvent;
// 繰り返し実行の設定
timer.AutoReset = true;
// タイマーの開始
timer.Enabled = true;
Console.WriteLine("System.Timers.Timer 実行中...");
Console.ReadLine();
timer.Stop();
timer.Dispose();
}
private static void OnTimedEvent(Object source, ElapsedEventArgs e)
{
Console.WriteLine($"{e.SignalTime:HH:mm:ss.fff} にイベントが発生しました。");
}
}
4. UIスレッド専用のタイマー:Forms と Dispatcher
デスクトップアプリケーション開発において、UI(ボタンやテキストボックス)を定期的に更新したい場合は、UIスレッド用のタイマーを使用するのが一般的です。
4.1 System.Windows.Forms.Timer (WinForms)
Windows Forms環境向けです。
メッセージループを利用して動作するため、完全にシングルスレッドで動作します。
- メリット: UIコントロールに直接アクセスでき、スレッド間通信を気にする必要がありません。
- デメリット: 処理が重いと画面がフリーズします。また、精度は低く、Windowsメッセージの混雑状況に左右されます。
4.2 System.Windows.Threading.DispatcherTimer (WPF/WinUI)
WPFやWinUIなどのDispatcherベースのフレームワークで使用されます。
DispatcherPriorityを指定できるため、レンダリング優先か、バックグラウンド優先かなどの制御が可能です。- 原理的には
Forms.Timerと同様、UIスレッドの空き時間に実行されます。
5. 【推奨】モダンな PeriodicTimer (.NET 6+)
.NET 6で導入された System.Threading.PeriodicTimer は、従来のタイマーとは全く異なる設計思想で作られています。
イベントやコールバックではなく、async/await を使ったループ構造でタイマー処理を記述します。
5.1 なぜ PeriodicTimer が推奨されるのか
従来のタイマーには「リエントラント(再入)」の問題がありました。
処理が予定の間隔よりも長引いた場合、前の処理が終わる前に次のイベントが発生してしまう現象です。
PeriodicTimer は、WaitForNextTickAsync() メソッドを待機することで、「前の処理が終わってから次の間隔を待つ」 という流れを自然に記述でき、リソース漏れも防ぎやすい設計になっています。
5.2 実装例
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using var cts = new CancellationTokenSource();
// 5秒後に停止させる
cts.CancelAfter(TimeSpan.FromSeconds(5));
// 1秒間隔のタイマー
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
try
{
// 次のティックが来るまで非同期で待機
while (await timer.WaitForNextTickAsync(cts.Token))
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss} - ティックを受信");
// ここで非同期処理を実行できる
await Task.Delay(100);
}
}
catch (OperationCanceledException)
{
Console.WriteLine("タイマーがキャンセルされました。");
}
}
}
15:30:01 - ティックを受信
15:30:02 - ティックを受信
15:30:03 - ティックを受信
15:30:04 - ティックを受信
15:30:05 - ティックを受信
タイマーがキャンセルされました。
6. タイマーを使い分けるための判断基準
どのタイマーを使うべきか迷った際は、以下のフローチャートを参考にしてください。
- 非同期ループ(async/await)を使いたいか?
- はい → PeriodicTimer が最適です。
- UI要素(ラベルやグラフ)を更新したいか?
- WinFormsなら →
System.Windows.Forms.Timer - WPF/WinUIなら →
DispatcherTimer
- WinFormsなら →
- バックグラウンドで高精度または高頻度な処理を行いたいか?
- 軽さ重視なら →
System.Threading.Timer - 扱いやすさ(イベント駆動)重視なら →
System.Timers.Timer
- 軽さ重視なら →
精度についての補足
Windows OS自体がリアルタイムOSではないため、いずれのタイマーもミリ秒単位の厳密な精度は保証されません。 システムの負荷状況により数ミリ秒〜数十ミリ秒の誤差(ジッター)が発生します。
より高精度な計測が必要な場合は、Stopwatch クラスを併用するか、マルチメディアタイマーなどの外部ライブラリを検討する必要があります。
7. 実装時の落とし穴とベストプラクティス
タイマーの実装において、多くの開発者がハマるポイントがいくつかあります。
7.1 リエントラント(二重起動)の防止
System.Timers.Timer や System.Threading.Timer を使う場合、処理が長引くと、前回の処理が終わっていないのに次の処理が始まってしまいます。
これを防ぐには、処理の開始時にタイマーを止め、終了時に再開する、あるいはフラグで制御するといった工夫が必要です。
private static int _isExecuting = 0;
private static void OnElapsed(object sender, ElapsedEventArgs e)
{
// すでに実行中なら何もしない (ロックの代わりにInterlockedを使用)
if (Interlocked.CompareExchange(ref _isExecuting, 1, 0) != 0)
{
return;
}
try
{
// 重い処理
}
finally
{
_isExecuting = 0; // 終了フラグを戻す
}
}
7.2 IDisposable の実装
タイマーはアンマネージドリソースを扱うため、使い終わったら必ず Dispose() を呼び出す必要があります。 特にクラスのメンバ変数としてタイマーを持つ場合は、そのクラス自体に IDisposable インターフェースを実装させ、確実に破棄されるようにしましょう。
7.3 ラムダ式とガベージコレクション
タイマーをメソッド内のローカル変数として定義し、ラムダ式でコールバックを記述すると、メソッドを抜けた後に変数への参照が失われ、タイマーがGCによって回収されて動かなくなることがあります。
タイマーを使用する期間中は、必ずフィールド変数などで参照を維持するようにしてください。
8. まとめ
C#のタイマーは、動作環境とスレッドモデルに応じて選択することが最も重要です。
- System.Threading.Timer:低コストなバックグラウンド処理用。
- System.Timers.Timer:サーバーサイドでのイベントベース処理用。
- UIタイマー (Forms/Dispatcher):デスクトップアプリの画面更新用。
- PeriodicTimer:最新の .NET における非同期ループ用。
特に、モダンな C# 開発においては、PeriodicTimer を活用することで、可読性が高く、リエントラントの問題を回避した安全な定期処理を実装できます。
それぞれのタイマーが持つ「スレッドの振る舞い」を正しく理解し、アプリケーションの要件に最適なクラスを選択してください。
