【C#】指定した間隔で処理を繰り返し実行する

目次

概要

一定の時間間隔で特定のメソッドを繰り返し呼び出すには、System.Threading.Timer クラスを使用します。 これはスレッドプール上のスレッドを使用してコールバックを実行するため、メインスレッドをブロックすることなく、バックグラウンドでの定期処理(ログ出力、状態監視など)を実現できます。

仕様(入出力)

  • 入力
    • 開始までの遅延時間(ミリ秒)
    • 繰り返す間隔(ミリ秒)
  • 出力
    • 指定した間隔ごとに、コンソールへ現在時刻を出力します。
  • 動作
    • ユーザーがEnterキーを押すまで動作を継続し、キー入力後にタイマーを停止して終了します。

基本の使い方

コンストラクタで「実行するメソッド」「メソッドへの引数」「開始までの遅延」「実行間隔」を指定します。

// 1秒(1000ms)後に開始し、その後2秒(2000ms)ごとに CallbackMethod を実行
Timer timer = new Timer(CallbackMethod, null, 1000, 2000);

コード全文

タイマーを作成し、別スレッドで定期的に時刻を表示し続けるコンソールアプリケーションです。メインスレッドは Console.ReadLine() でユーザーの終了指示を待ちます。

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] アプリケーション開始");

        // タイマーの生成
        // 第1引数: 実行するメソッド (TimerCallback)
        // 第2引数: メソッドに渡すデータ (今回はnull)
        // 第3引数: 最初の実行までの遅延時間 (ミリ秒) -> 1000ms後に初回実行
        // 第4引数: 繰り返す間隔 (ミリ秒) -> 以降2000msごとに実行
        using (var timer = new Timer(DoSomething, null, 1000, 2000))
        {
            Console.WriteLine("Enterキーを押すとタイマーを停止して終了します...");
            
            // ユーザー入力を待機(これがないとプログラムが即終了してしまう)
            Console.ReadLine();

            // タイマーの停止
            // Timeout.Infinite (-1) を指定すると、タイマーが無効化される
            timer.Change(Timeout.Infinite, Timeout.Infinite);
            
            Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] タイマー停止指示");
        }
        // usingブロックを抜けると Dispose() が呼ばれ、タイマーリソースが解放される

        Console.WriteLine("アプリケーションが終了しました。");
    }

    // 定期実行されるコールバックメソッド
    // 引数は object? 型で受け取る必要がある
    static void DoSomething(object? state)
    {
        // 現在のスレッドIDも表示して、メインスレッドとは異なることを確認
        int threadId = Thread.CurrentThread.ManagedThreadId;
        Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 定期処理を実行中 (Thread: {threadId})");
    }
}

実行結果例

[12:00:00] アプリケーション開始
Enterキーを押すとタイマーを停止して終了します...
[12:00:01] 定期処理を実行中 (Thread: 4)
[12:00:03] 定期処理を実行中 (Thread: 5)
[12:00:05] 定期処理を実行中 (Thread: 4)
(Enterキーを押下)
[12:00:06] タイマー停止指示
アプリケーションが終了しました。

カスタムポイント

  • スケジュールの変更
    • timer.Change(dueTime, period) メソッドを使えば、実行中に間隔を変更したり、一時停止したりできます。
  • 状態オブジェクトの受け渡し
    • コンストラクタの第2引数(state)に任意のオブジェクトを渡すと、コールバックメソッドの引数 arg として受け取ることができます。カウンターや設定クラスを渡す際に便利です。

注意点

  1. リエントラント(再入)性
    • 処理に時間がかかり、次の実行タイミングが来てしまった場合、前の処理が終わる前に別のスレッドで次の処理が開始されることがあります(多重実行)。これを防ぐには、メソッド内でロック(lock)を取得するか、タイマーを「ワンショット(間隔なし)」で設定し、処理終了後に再度 Change で次回の予約をする設計が必要です。
  2. 例外処理
    • コールバックメソッド内で発生した未処理の例外は、プロセス全体をクラッシュさせます。必ず try-catch ブロックで例外を捕捉してください。
  3. GCによる回収
    • ローカル変数として Timer を作成し、using や待機処理なしに関数を抜けると、タイマー自体がガベージコレクションの対象となり、勝手に止まってしまうことがあります。タイマーのインスタンスはクラスのフィールド等で保持するか、コード例のようにスコープを維持してください。

応用

リエントラントを防ぐパターン

処理中は次回のタイマーを止め、処理が終わってから次回のタイマーをセットすることで、重複実行を確実に防ぐ実装です。

class SafeTimer
{
    private Timer _timer;

    public void Start()
    {
        // 最初は1秒後に1回だけ実行(周期は Infinite)
        _timer = new Timer(Callback, null, 1000, Timeout.Infinite);
    }

    private void Callback(object state)
    {
        try
        {
            Console.WriteLine("重い処理を開始...");
            Thread.Sleep(3000); // 処理時間のシミュレーション
            Console.WriteLine("処理終了");
        }
        finally
        {
            // 処理が終わってから次のタイマーを予約(2秒後)
            _timer?.Change(2000, Timeout.Infinite);
        }
    }
}

まとめ

System.Threading.Timer は軽量でサーバーサイドやバックグラウンド処理に適したタイマーです。WindowsフォームやWPFのUIコンポーネントを操作する場合は System.Windows.Forms.Timer などのUI専用タイマーを使うか、Invoke でスレッドを切り替える必要があります。また、処理が重なる多重実行のリスクがあるため、処理内容に応じてロックやワンショット実行のパターンを適切に選択して実装してください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次