C#で一定間隔ごとに処理を実行したい場合、多くの開発者が最初に思い浮かべるのがTimerクラスです。

しかし、C#のクラスライブラリには名前空間の異なる複数のタイマーが存在しており、それぞれ動作するスレッドモデルや精度、用途が大きく異なります。

適切なタイマーを選択しないと、UIフリーズの原因になったり、意図しないマルチスレッドによる競合状態(レースコンディション)を招いたりする可能性があります。

本記事では、C#で利用可能な主要な4つのタイマーに、モダンな非同期処理で推奨されるPeriodicTimerを加えた特性を整理し、具体的なコード例とともに使い分け方を詳しく解説します。

1. C#におけるタイマーの全体像

.NET環境において、定期的な処理を実現するためのクラスは主に4種類あります。

これらは一見似ていますが、「どのスレッドで実行されるか」という点が最大の相違点です。

まずは、それぞれのタイマーが属する名前空間と、主な実行環境を整理しましょう。

タイマー名名前空間実行スレッド主な用途
System.Threading.TimerSystem.Threadingスレッドプールバックグラウンドでの軽量な処理
System.Timers.TimerSystem.Timersスレッドプール (既定)サーバーサイドや汎用的なイベント処理
System.Windows.Forms.TimerSystem.Windows.FormsUIスレッドWindows FormsアプリのUI更新
System.Windows.Threading.DispatcherTimerSystem.Windows.ThreadingUIスレッド (Dispatcher)WPFやWinUIのUI更新

これらに加え、.NET 6以降では非同期プログラミングに最適化された System.Threading.PeriodicTimer も重要な選択肢となっています。

2. System.Threading.Timer:軽量なスレッドプールタイマー

System.Threading.Timer は、もっとも基本的かつ軽量なタイマーです。

指定した時間が経過すると、スレッドプール上のスレッドでコールバックメソッドが実行されます。

2.1 特徴と注意点

このタイマーは、オーバーヘッドが非常に小さいため、大量のタイマーを同時に動かす必要がある場合に適しています。

しかし、以下の点に注意が必要です。

  • コールバックはスレッドプールで実行されるため、UIコントロールに直接アクセスすることはできません。
  • コールバック内で重い処理を行うと、スレッドプールを圧迫する可能性があります。
  • タイマーのインスタンスがガベージコレクション(GC)によって回収されないよう、参照を保持し続ける必要があります。

2.2 実装例

C#
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 実装例

C#
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 実装例

C#
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. タイマーを使い分けるための判断基準

どのタイマーを使うべきか迷った際は、以下のフローチャートを参考にしてください。

  1. 非同期ループ(async/await)を使いたいか?
    • はい → PeriodicTimer が最適です。
  2. UI要素(ラベルやグラフ)を更新したいか?
    • WinFormsなら → System.Windows.Forms.Timer
    • WPF/WinUIなら → DispatcherTimer
  3. バックグラウンドで高精度または高頻度な処理を行いたいか?
    • 軽さ重視なら → System.Threading.Timer
    • 扱いやすさ(イベント駆動)重視なら → System.Timers.Timer

精度についての補足

Windows OS自体がリアルタイムOSではないため、いずれのタイマーもミリ秒単位の厳密な精度は保証されません。 システムの負荷状況により数ミリ秒〜数十ミリ秒の誤差(ジッター)が発生します。

より高精度な計測が必要な場合は、Stopwatch クラスを併用するか、マルチメディアタイマーなどの外部ライブラリを検討する必要があります。

7. 実装時の落とし穴とベストプラクティス

タイマーの実装において、多くの開発者がハマるポイントがいくつかあります。

7.1 リエントラント(二重起動)の防止

System.Timers.TimerSystem.Threading.Timer を使う場合、処理が長引くと、前回の処理が終わっていないのに次の処理が始まってしまいます。

これを防ぐには、処理の開始時にタイマーを止め、終了時に再開する、あるいはフラグで制御するといった工夫が必要です。

C#
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 を活用することで、可読性が高く、リエントラントの問題を回避した安全な定期処理を実装できます。

それぞれのタイマーが持つ「スレッドの振る舞い」を正しく理解し、アプリケーションの要件に最適なクラスを選択してください。