C# Timer Elapsedイベントが動かない・重複する原因と正しい使い方を徹底解説

はじめに

C#で一定間隔の処理を実行したいときによく使われるのがTimerです。特にSystem.Timers.Timerでは、指定した時間が経過するとElapsedイベントが発生し、そのタイミングで処理を実行できます。

しかし、実際に使ってみると次のようなトラブルが起きることがあります。

「C#のTimerでElapsedイベントが動かない」

「Elapsedイベントが1回しか実行されない」

「同じ処理が重複して実行される」

「UIを更新しようとしたらエラーになる」

「async処理を書いたら多重実行されてしまう」

これらの問題は、Timerの仕組み、Elapsedイベントの実行スレッド、AutoReset、イベント登録、インスタンスのライフサイクルを正しく理解していないことが原因で起こるケースが多いです。

この記事では、C#のTimerElapsedイベントの基本から、動かない原因、重複実行される原因、安全な実装方法、実践的なサンプルコードまで詳しく解説します。

1. C# Timer Elapsedイベントとは

1-1. Elapsedイベントの基本的な役割

Elapsedイベントは、System.Timers.Timerで設定した時間間隔が経過したときに発生するイベントです。

たとえば、5秒ごとにログを出力したい場合、Interval5000を設定し、Elapsedイベントに実行したい処理を登録します。

C#
var timer = new System.Timers.Timer();
timer.Interval = 5000;
timer.Elapsed += OnElapsed;
timer.Start();

static void OnElapsed(object? sender, System.Timers.ElapsedEventArgs e)
{
Console.WriteLine("5秒経過しました");
}

Elapsedイベントは、バックグラウンドで一定間隔の処理を実行したい場合に便利です。

代表的な用途には、次のようなものがあります。

・定期的なログ出力
・ファイル監視
・ステータスチェック
・バッチ処理
・APIの定期呼び出し
・キャッシュ更新
・一定時間後の処理実行

1-2. System.Timers.Timerの特徴

System.Timers.Timerは、サーバーアプリケーションやコンソールアプリケーションなどで使いやすいTimerです。

主な特徴は次のとおりです。

・Elapsedイベントで定期処理を実行できる
・Intervalはミリ秒単位で指定する
・AutoReset=trueなら繰り返し実行される
・AutoReset=falseなら1回だけ実行される
・Elapsedイベントは通常、別スレッドで実行される
・UIスレッド上で動くTimerではない

特に重要なのは、ElapsedイベントがUIスレッドではなく別スレッドで実行される点です。

そのため、Windows FormsやWPFで画面を直接更新すると、スレッド関連のエラーが発生することがあります。

1-3. Tickイベントとの違い

C#のTimerには、Elapsedイベントを使うものとTickイベントを使うものがあります。

Elapsedイベントは主にSystem.Timers.Timerで使います。

一方、Tickイベントは主にSystem.Windows.Forms.TimerDispatcherTimerで使います。

C#
// System.Timers.Timer
timer.Elapsed += OnElapsed;

// Windows Forms Timer
timer.Tick += OnTick;

大きな違いは、イベントが実行されるスレッドです。

System.Windows.Forms.TimerTickイベントは、基本的にWindows FormsのUIスレッドで実行されます。

そのため、フォーム上のラベルやボタンなどを直接更新できます。

一方、System.Timers.TimerElapsedイベントは、通常ThreadPoolのスレッドで実行されます。

そのため、UIを直接更新する場合はInvokeDispatcherが必要です。

1-4. Timerで定期処理を実行する仕組み

System.Timers.Timerは、内部的に指定されたIntervalを監視し、時間が経過するとElapsedイベントを発生させます。

処理の流れは次のようになります。

1. Timerインスタンスを生成する
2. Intervalを設定する
3. Elapsedイベントに処理を登録する
4. StartまたはEnabled=trueでTimerを開始する
5. 指定時間が経過する
6. Elapsedイベントが発生する
7. 登録されたイベントハンドラが実行される
8. AutoReset=trueなら再び次のIntervalを待つ

つまり、Timerを作っただけでは処理は実行されません。

IntervalElapsedイベント登録、StartまたはEnabled=trueがそろって初めて動作します。

2. C# Timer Elapsedイベントの基本的な使い方

2-1. Timerの生成とIntervalの設定

System.Timers.Timerを使うには、まずインスタンスを生成し、Intervalを設定します。

C#
var timer = new System.Timers.Timer();
timer.Interval = 1000;

Intervalの単位はミリ秒です。

1000  = 1秒
5000 = 5秒
60000 = 1分

「10秒ごと」に実行したい場合は、10ではなく10000を設定します。

C#
timer.Interval = 10000;

2-2. Elapsedイベントハンドラの登録方法

Elapsedイベントには、実行したいメソッドを登録します。

C#
timer.Elapsed += Timer_Elapsed;

イベントハンドラは次の形式で定義します。

C#
private static void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
{
Console.WriteLine("Timer Elapsed");
}

ラムダ式でも登録できます。

C#
timer.Elapsed += (sender, e) =>
{
Console.WriteLine("Elapsedイベントが発生しました");
};

ただし、ラムダ式で登録すると解除しにくくなるため、重複登録を防ぎたい場合は名前付きメソッドを使う方が安全です。

2-3. Start・Stop・Enabledの違い

System.Timers.Timerには、開始・停止に関係するメソッドやプロパティがあります。

C#
timer.Start();
timer.Stop();

timer.Enabled = true;
timer.Enabled = false;

Start()Enabled = trueにするのとほぼ同じです。

Stop()Enabled = falseにするのとほぼ同じです。

C#
timer.Start();        // Timerを開始
timer.Enabled = true; // Timerを開始

timer.Stop(); // Timerを停止
timer.Enabled = false;// Timerを停止

一般的には、読みやすさを重視してStart()Stop()を使うことが多いです。

2-4. AutoResetの役割と設定方法

AutoResetは、Elapsedイベントを繰り返し発生させるかどうかを決めるプロパティです。

C#
timer.AutoReset = true;

AutoReset=trueの場合、Intervalごとに繰り返しElapsedイベントが発生します。

1秒後 → 実行
1秒後 → 実行
1秒後 → 実行

AutoReset=falseの場合、Elapsedイベントは1回だけ発生します。

C#
timer.AutoReset = false;
1秒後 → 実行
その後は停止

1回だけ実行したい場合はAutoReset=false、定期的に実行したい場合はAutoReset=trueを使います。

2-5. 正常に動作する最小サンプルコード

次のコードは、1秒ごとにElapsedイベントを実行する最小サンプルです。

C#
using System;
using System.Timers;

class Program
{
private static readonly Timer _timer = new Timer();

static void Main()
{
_timer.Interval = 1000;
_timer.AutoReset = true;
_timer.Elapsed += Timer_Elapsed;
_timer.Start();

Console.WriteLine("Timerを開始しました。Enterキーで終了します。");
Console.ReadLine();

_timer.Stop();
_timer.Dispose();
}

private static void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
Console.WriteLine($"Elapsed: {DateTime.Now:HH:mm:ss.fff}");
}
}

ポイントは、Timerをローカル変数ではなく静的フィールドとして保持している点です。

これにより、Timerインスタンスが途中で破棄されるリスクを減らせます。

3. C# Timer Elapsedイベントが動かない主な原因

3-1. StartまたはEnabled=trueを設定していない

Timerは生成しただけでは動きません。

次のコードでは、Elapsedイベントは実行されません。

C#
var timer = new System.Timers.Timer(1000);
timer.Elapsed += Timer_Elapsed;
// Startしていないため動かない

必ずStart()またはEnabled = trueを呼び出します。

C#
timer.Start();

または次のように書きます。

C#
timer.Enabled = true;

3-2. Elapsedイベントを登録していない

Timerを開始していても、Elapsedイベントに処理を登録していなければ、実行される処理はありません。

C#
var timer = new System.Timers.Timer(1000);
timer.Start();
// Elapsedイベントを登録していない

正しくは次のように登録します。

C#
timer.Elapsed += Timer_Elapsed;
timer.Start();

3-3. TimerインスタンスがGCで破棄されている

Timerをメソッド内のローカル変数として作成し、その後参照を保持していない場合、ガベージコレクションの対象になる可能性があります。

C#
void StartTimer()
{
var timer = new System.Timers.Timer(1000);
timer.Elapsed += Timer_Elapsed;
timer.Start();
}

このコードでは、StartTimerメソッドが終了した後、timerを参照する変数がなくなります。

そのため、期待どおりに動作し続けないことがあります。

安全に使うには、クラスのフィールドとして保持します。

C#
private readonly System.Timers.Timer _timer = new System.Timers.Timer(1000);

public void Start()
{
_timer.Elapsed += Timer_Elapsed;
_timer.Start();
}

3-4. Intervalの単位を誤解している

Intervalの単位は秒ではなくミリ秒です。

C#
timer.Interval = 10;

これは10秒ではなく、10ミリ秒です。

10秒にしたい場合は、次のように書きます。

C#
timer.Interval = 10000;

可読性を上げるなら、TimeSpanを使って次のように書くのがおすすめです。

C#
timer.Interval = TimeSpan.FromSeconds(10).TotalMilliseconds;

3-5. AutoReset=falseで1回しか実行されない

AutoReset=falseに設定すると、Elapsedイベントは1回だけ発生します。

C#
timer.AutoReset = false;

「最初の1回だけ動いて、その後動かない」という場合は、AutoResetの値を確認してください。

繰り返し実行したい場合は、次のようにします。

C#
timer.AutoReset = true;

ただし、長時間処理ではAutoReset=trueのまま使うと重複実行の原因になることがあります。

3-6. 例外発生により処理が止まっている

Elapsedイベント内で例外が発生していると、処理が途中で終了している可能性があります。

C#
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
// ここで例外が発生すると以降の処理が実行されない
DoWork();
}

Elapsedイベント内では、必ず例外処理を入れることをおすすめします。

C#
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
try
{
DoWork();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}

特に本番環境では、例外を握りつぶすのではなく、ログに出力するようにしましょう。

3-7. アプリケーション終了によりTimerが実行されない

コンソールアプリでTimerを開始しても、メインスレッドがすぐに終了すると、Elapsedイベントが発生する前にアプリケーション自体が終了します。

C#
static void Main()
{
var timer = new System.Timers.Timer(1000);
timer.Elapsed += Timer_Elapsed;
timer.Start();

// すぐにMainが終了する
}

この場合、Console.ReadLine()などでアプリケーションを終了させないようにします。

C#
static void Main()
{
var timer = new System.Timers.Timer(1000);
timer.Elapsed += Timer_Elapsed;
timer.Start();

Console.ReadLine();
}

4. C# Timer Elapsedイベントが重複して実行される原因

4-1. Elapsedイベントを複数回登録している

Elapsedイベントが重複実行される最も多い原因のひとつが、イベントハンドラの複数回登録です。

C#
timer.Elapsed += Timer_Elapsed;
timer.Elapsed += Timer_Elapsed;
timer.Elapsed += Timer_Elapsed;

この場合、1回のElapsedイベント発生でTimer_Elapsedが3回呼ばれます。

ボタンのクリックや初期化処理のたびに次のようなコードを実行している場合は注意が必要です。

C#
private void StartButton_Click(object sender, EventArgs e)
{
timer.Elapsed += Timer_Elapsed;
timer.Start();
}

このコードでは、ボタンを押すたびにイベントが追加登録されます。

対策として、イベント登録は初期化時に1回だけ行います。

C#
timer.Elapsed -= Timer_Elapsed;
timer.Elapsed += Timer_Elapsed;

このように、一度解除してから登録する方法もあります。

4-2. 処理時間がIntervalより長い

Elapsedイベント内の処理時間がIntervalより長い場合、前回の処理が終わる前に次のElapsedイベントが発生することがあります。

たとえば、Intervalが1秒で、処理に3秒かかる場合です。

0秒  処理開始
1秒 次のElapsed発生
2秒 次のElapsed発生
3秒 最初の処理終了

このような状態になると、同じ処理が並列に実行され、重複処理やデータ不整合の原因になります。

4-3. AutoReset=trueのまま長時間処理をしている

AutoReset=trueは、一定間隔で自動的にElapsedイベントを発生させる設定です。

短い処理であれば問題ありませんが、処理時間が不安定な場合は注意が必要です。

C#
timer.AutoReset = true;
timer.Interval = 1000;

この状態でElapsedイベント内の処理に5秒かかると、前の処理が終わる前に次の処理が始まる可能性があります。

長時間処理や非同期処理を行う場合は、AutoReset=falseにして、処理完了後に手動で再開する方法が安全です。

4-4. Startを何度も呼び出している

System.Timers.Timerでは、同じTimerインスタンスに対してStart()を何度呼んでも、それだけでイベントハンドラが増えるわけではありません。

ただし、次のようなコードでは重複の原因になります。

C#
void StartTimer()
{
var timer = new System.Timers.Timer(1000);
timer.Elapsed += Timer_Elapsed;
timer.Start();
}

このメソッドを何度も呼ぶと、新しいTimerインスタンスが複数作成され、それぞれがElapsedイベントを発生させます。

また、Start()の前に毎回イベント登録している場合も危険です。

C#
void StartTimer()
{
_timer.Elapsed += Timer_Elapsed;
_timer.Start();
}

この場合、StartTimer()を呼ぶたびにイベントハンドラが追加されます。

4-5. 非同期処理で多重実行が発生している

Elapsedイベントでasync処理を呼ぶ場合、処理の完了を待たずに次のElapsedイベントが発生することがあります。

C#
private async void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
await DoWorkAsync();
}

async voidはイベントハンドラでは使われることがありますが、例外処理や多重実行制御が難しくなります。

特にAutoReset=trueのまま非同期処理を実行すると、前回のawaitが完了する前に次のイベントが実行される可能性があります。

4-6. Timerを複数インスタンス生成している

意図せず複数のTimerを作成している場合も、Elapsedイベントが重複しているように見えます。

C#
var timer1 = new System.Timers.Timer(1000);
var timer2 = new System.Timers.Timer(1000);
var timer3 = new System.Timers.Timer(1000);

それぞれのTimerに同じ処理を登録して開始すると、同じタイミングで複数回処理が実行されます。

DIコンテナ、フォームの再生成、サービスの再起動処理などで、同じTimerが複数作られていないか確認しましょう。

5. Elapsedイベントの重複実行を防ぐ正しい実装方法

5-1. イベント登録を1回だけにする

イベント登録は、コンストラクタや初期化メソッドで1回だけ行うのが基本です。

C#
public class MyService : IDisposable
{
private readonly System.Timers.Timer _timer;

public MyService()
{
_timer = new System.Timers.Timer(1000);
_timer.Elapsed += Timer_Elapsed;
_timer.AutoReset = true;
}

public void Start()
{
_timer.Start();
}

private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
Console.WriteLine("実行");
}

public void Dispose()
{
_timer.Elapsed -= Timer_Elapsed;
_timer.Dispose();
}
}

Start()のたびにElapsed +=を書かないことが重要です。

5-2. 処理中フラグで多重実行を防ぐ

簡単な多重実行防止には、処理中フラグを使う方法があります。

C#
private bool _isRunning = false;

private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
if (_isRunning)
{
return;
}

try
{
_isRunning = true;

Console.WriteLine("処理開始");
Thread.Sleep(3000);
Console.WriteLine("処理終了");
}
finally
{
_isRunning = false;
}
}

ただし、複数スレッドから同時にアクセスされる可能性があるため、厳密にはlockInterlockedを使う方が安全です。

5-3. Stopしてから処理し、完了後にStartする

重複実行を確実に避けたい場合は、Elapsedイベントの先頭でTimerを停止し、処理完了後に再開します。

C#
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
var timer = (System.Timers.Timer?)sender;
if (timer == null)
{
return;
}

timer.Stop();

try
{
Console.WriteLine("処理開始");
Thread.Sleep(3000);
Console.WriteLine("処理終了");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
finally
{
timer.Start();
}
}

この方法では、処理中に次のElapsedイベントが発生しにくくなります。

ただし、完全に排他制御したい場合はlockSemaphoreSlimも併用します。

5-4. AutoReset=falseを使って制御する

長時間処理を安全に行う場合は、AutoReset=falseがおすすめです。

C#
_timer.AutoReset = false;

AutoReset=falseにすると、Elapsedイベントは1回発生した後に自動で停止します。

処理が終わったタイミングで手動で再開します。

C#
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
try
{
Console.WriteLine("処理開始");
Thread.Sleep(3000);
Console.WriteLine("処理終了");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
finally
{
_timer.Start();
}
}

この方法は、「前回の処理が終わってから次のカウントを開始したい」場合に適しています。

5-5. lockやSemaphoreSlimを使う方法

複数スレッドで同時実行される可能性がある処理では、lockを使って排他制御できます。

C#
private readonly object _lock = new object();

private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
lock (_lock)
{
Console.WriteLine("処理開始");
Thread.Sleep(3000);
Console.WriteLine("処理終了");
}
}

ただし、lockの場合、次の処理が待機してしまうため、処理が詰まる可能性があります。

「処理中なら次の実行はスキップしたい」場合は、Monitor.TryEnterが便利です。

C#
private readonly object _lock = new object();

private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
if (!Monitor.TryEnter(_lock))
{
return;
}

try
{
Console.WriteLine("処理開始");
Thread.Sleep(3000);
Console.WriteLine("処理終了");
}
finally
{
Monitor.Exit(_lock);
}
}

非同期処理ではSemaphoreSlimを使うのが一般的です。

C#
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

private async void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
if (!await _semaphore.WaitAsync(0))
{
return;
}

try
{
await DoWorkAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
finally
{
_semaphore.Release();
}
}

5-6. async/await利用時の注意点

Elapsedイベントでasync/awaitを使う場合は、次の点に注意します。

・async voidになるため例外処理を必ず入れる
・AutoReset=trueだと多重実行されやすい
・処理中に次のElapsedが発生しないように制御する
・SemaphoreSlimなどで排他制御する
・Timerの停止と再開を明確にする

安全な例は次のとおりです。

C#
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

private async void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
if (!await _semaphore.WaitAsync(0))
{
return;
}

try
{
await DoWorkAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
finally
{
_semaphore.Release();
}
}

private async Task DoWorkAsync()
{
await Task.Delay(3000);
Console.WriteLine("非同期処理完了");
}

より安全に制御したい場合は、AutoReset=falseにして、非同期処理完了後にStart()します。

6. C# Timer使用時に注意すべきスレッドの問題

6-1. Elapsedイベントは別スレッドで実行される

System.Timers.TimerElapsedイベントは、通常ThreadPoolのスレッドで実行されます。

つまり、Elapsedイベント内の処理は、メインスレッドやUIスレッドとは別のスレッドで動く可能性があります。

C#
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}

このコードを実行すると、メインスレッドとは異なるスレッドIDが表示されることがあります。

この性質により、サーバー処理やバックグラウンド処理には便利ですが、UI更新には注意が必要です。

6-2. Windows FormsやWPFのUI更新でエラーになる理由

Windows FormsやWPFでは、UI部品はUIスレッドから操作する必要があります。

Elapsedイベントは別スレッドで実行されるため、次のようなコードはエラーになる可能性があります。

C#
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
label1.Text = DateTime.Now.ToString();
}

Windows Formsでは、別スレッドからコントロールを操作すると「有効ではないスレッド間の操作」のようなエラーが発生することがあります。

WPFでも、別スレッドからUI要素を操作すると例外が発生します。

6-3. Invoke・Dispatcherを使ったUI更新方法

Windows FormsでUIを更新する場合は、InvokeまたはBeginInvokeを使います。

C#
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
if (label1.InvokeRequired)
{
label1.Invoke(new Action(() =>
{
label1.Text = DateTime.Now.ToString("HH:mm:ss");
}));
}
else
{
label1.Text = DateTime.Now.ToString("HH:mm:ss");
}
}

WPFの場合は、Dispatcherを使います。

C#
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
Dispatcher.Invoke(() =>
{
TextBlockTime.Text = DateTime.Now.ToString("HH:mm:ss");
});
}

UIアプリで定期的に画面を更新するだけなら、System.Timers.Timerではなく、System.Windows.Forms.TimerDispatcherTimerを使う方がシンプルです。

6-4. スレッドセーフに処理を書くポイント

Elapsedイベント内では、複数スレッドから同じ変数やリソースにアクセスする可能性があります。

そのため、次の点に注意します。

・共有変数の読み書きに注意する
・必要に応じてlockを使う
・ListやDictionaryなどのコレクション操作に注意する
・ファイル書き込みの競合に注意する
・DB更新の二重実行に注意する
・処理中にTimerがDisposeされるケースを考慮する

特に、Elapsedイベントが重複実行される可能性がある場合、共有変数の値が想定外になることがあります。

6-5. 共有変数を扱う場合の注意点

次のようなカウンター処理は、一見問題なさそうに見えます。

C#
private int _count = 0;

private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
_count++;
}

しかし、複数スレッドから同時に実行されると、正しくカウントされない可能性があります。

このような場合は、Interlockedを使います。

C#
private int _count = 0;

private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
int current = Interlocked.Increment(ref _count);
Console.WriteLine(current);
}

複雑な共有状態を扱う場合は、lockで保護します。

C#
private readonly object _lock = new object();
private int _count = 0;

private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
lock (_lock)
{
_count++;
Console.WriteLine(_count);
}
}

7. C#で使えるTimerの種類と使い分け

7-1. System.Timers.Timer

System.Timers.Timerは、Elapsedイベントを使って定期処理を実行するTimerです。

C#
var timer = new System.Timers.Timer(1000);
timer.Elapsed += Timer_Elapsed;
timer.Start();

バックグラウンド処理、サービス、コンソールアプリ、サーバー処理などに向いています。

ただし、UI更新を直接行う用途には向いていません。

7-2. System.Threading.Timer

System.Threading.Timerは、より低レベルなTimerです。

コールバックメソッドを指定して、一定時間後または一定間隔で処理を実行します。

C#
var timer = new System.Threading.Timer(_ =>
{
Console.WriteLine("実行");
}, null, 0, 1000);

Elapsedイベントではなく、コールバックを使います。

軽量で柔軟ですが、開始・停止やDisposeの管理を明確に行う必要があります。

7-3. System.Windows.Forms.Timer

System.Windows.Forms.Timerは、Windows Formsアプリで使うTimerです。

TickイベントはUIスレッドで実行されるため、フォーム上のコントロールを直接更新できます。

C#
var timer = new System.Windows.Forms.Timer();
timer.Interval = 1000;
timer.Tick += (s, e) =>
{
label1.Text = DateTime.Now.ToString("HH:mm:ss");
};
timer.Start();

Windows Formsで画面更新を行うなら、このTimerが使いやすいです。

7-4. DispatcherTimer

DispatcherTimerは、WPFアプリで使うTimerです。

TickイベントはWPFのDispatcherスレッドで実行されるため、UI要素を直接更新できます。

C#
var timer = new System.Windows.Threading.DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(1);
timer.Tick += (s, e) =>
{
TextBlockTime.Text = DateTime.Now.ToString("HH:mm:ss");
};
timer.Start();

WPFで画面表示を定期更新する場合は、DispatcherTimerが適しています。

7-5. PeriodicTimer

PeriodicTimerは、.NET 6以降で使えるTimerです。

async/awaitと相性が良く、非同期の定期処理を書きやすいのが特徴です。

C#
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));

while (await timer.WaitForNextTickAsync())
{
Console.WriteLine("実行");
}

非同期処理を安全にループで実行したい場合は、System.Timers.TimerよりもPeriodicTimerの方が扱いやすいことがあります。

7-6. 用途別のおすすめTimer

用途に応じたTimerの選び方は次のとおりです。

コンソールアプリの定期処理
→ System.Timers.Timer / PeriodicTimer

Windows FormsのUI更新
→ System.Windows.Forms.Timer

WPFのUI更新
→ DispatcherTimer

非同期処理を定期実行
→ PeriodicTimer

軽量なバックグラウンド処理
→ System.Threading.Timer

Elapsedイベントを使いたい
→ System.Timers.Timer

C# Timer Elapsedで検索している場合、多くはSystem.Timers.Timerの使い方に関する疑問です。

ただし、UIアプリや非同期処理では、別のTimerを選んだ方が安全な場合もあります。

8. Elapsedイベントの実践的なサンプルコード

8-1. 一定間隔でログを出力するサンプル

次のコードは、2秒ごとにログを出力するサンプルです。

C#
using System;
using System.Timers;

class Program
{
private static readonly Timer _timer = new Timer(2000);

static void Main()
{
_timer.AutoReset = true;
_timer.Elapsed += Timer_Elapsed;
_timer.Start();

Console.WriteLine("ログ出力Timerを開始しました。Enterで終了します。");
Console.ReadLine();

_timer.Stop();
_timer.Dispose();
}

private static void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] ログ出力");
}
}

ポイントは、Timerをフィールドとして保持し、終了時にDispose()していることです。

8-2. 重複実行を防ぐサンプル

次のコードは、Monitor.TryEnterを使って重複実行を防ぐ例です。

C#
using System;
using System.Threading;
using System.Timers;

class Program
{
private static readonly Timer _timer = new Timer(1000);
private static readonly object _lock = new object();

static void Main()
{
_timer.AutoReset = true;
_timer.Elapsed += Timer_Elapsed;
_timer.Start();

Console.WriteLine("Enterで終了します。");
Console.ReadLine();

_timer.Dispose();
}

private static void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
if (!Monitor.TryEnter(_lock))
{
Console.WriteLine("前回処理中のためスキップ");
return;
}

try
{
Console.WriteLine($"処理開始: {DateTime.Now:HH:mm:ss.fff}");
Thread.Sleep(3000);
Console.WriteLine($"処理終了: {DateTime.Now:HH:mm:ss.fff}");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
finally
{
Monitor.Exit(_lock);
}
}
}

Intervalは1秒ですが、処理には3秒かかります。

それでも、前回処理中の場合はスキップするため、多重実行を防げます。

8-3. 1回だけ実行するサンプル

1回だけ実行したい場合は、AutoReset=falseを使います。

C#
using System;
using System.Timers;

class Program
{
private static readonly Timer _timer = new Timer(3000);

static void Main()
{
_timer.AutoReset = false;
_timer.Elapsed += Timer_Elapsed;
_timer.Start();

Console.WriteLine("3秒後に1回だけ実行します。Enterで終了します。");
Console.ReadLine();

_timer.Dispose();
}

private static void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
Console.WriteLine("1回だけ実行されました");
}
}

AutoReset=falseでは、Elapsedイベントが1回発生した後、Timerは自動的に停止します。

8-4. async処理を安全に実行するサンプル

非同期処理を安全に実行する場合は、AutoReset=falseSemaphoreSlimを組み合わせると安定します。

C#
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;

class Program
{
private static readonly Timer _timer = new Timer(1000);
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

static void Main()
{
_timer.AutoReset = false;
_timer.Elapsed += Timer_Elapsed;
_timer.Start();

Console.WriteLine("Enterで終了します。");
Console.ReadLine();

_timer.Dispose();
_semaphore.Dispose();
}

private static async void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
if (!await _semaphore.WaitAsync(0))
{
return;
}

try
{
Console.WriteLine($"非同期処理開始: {DateTime.Now:HH:mm:ss.fff}");
await DoWorkAsync();
Console.WriteLine($"非同期処理終了: {DateTime.Now:HH:mm:ss.fff}");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
finally
{
_semaphore.Release();
_timer.Start();
}
}

private static async Task DoWorkAsync()
{
await Task.Delay(3000);
}
}

このコードでは、処理が完了してから次のTimerを開始します。

そのため、非同期処理が長引いても重複実行されにくくなります。

8-5. UIアプリで安全に更新するサンプル

Windows FormsでSystem.Timers.Timerを使ってUIを更新する場合は、Invokeを使います。

C#
private readonly System.Timers.Timer _timer = new System.Timers.Timer(1000);

private void Form1_Load(object sender, EventArgs e)
{
_timer.AutoReset = true;
_timer.Elapsed += Timer_Elapsed;
_timer.Start();
}

private void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
{
if (label1.InvokeRequired)
{
label1.Invoke(new Action(() =>
{
label1.Text = DateTime.Now.ToString("HH:mm:ss");
}));
}
else
{
label1.Text = DateTime.Now.ToString("HH:mm:ss");
}
}

private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
_timer.Stop();
_timer.Dispose();
}

ただし、単純に画面を更新したいだけなら、Windows FormsではSystem.Windows.Forms.Timerを使う方が自然です。

C#
private readonly System.Windows.Forms.Timer _timer = new System.Windows.Forms.Timer();

private void Form1_Load(object sender, EventArgs e)
{
_timer.Interval = 1000;
_timer.Tick += (s, ev) =>
{
label1.Text = DateTime.Now.ToString("HH:mm:ss");
};
_timer.Start();
}

9. C# Timer Elapsedイベントのトラブルシューティング

9-1. まず確認すべきチェックリスト

C# Timer Elapsedが動かない、または重複する場合は、まず次の項目を確認してください。

・TimerをStartしているか
・Enabled=trueになっているか
・Elapsedイベントを登録しているか
・イベントを複数回登録していないか
・Intervalの単位を誤解していないか
・AutoResetの設定は正しいか
・Timerインスタンスを保持しているか
・アプリケーションがすぐ終了していないか
・Elapsed内で例外が発生していないか
・処理時間がIntervalより長くないか
・Timerを複数作成していないか
・UIを別スレッドから直接更新していないか

このチェックリストを確認するだけで、多くの問題は解決できます。

9-2. 動かない場合の確認手順

Elapsedイベントが動かない場合は、次の順番で確認します。

まず、Start()が呼ばれているか確認します。

C#
timer.Start();

次に、Elapsedイベントが登録されているか確認します。

C#
timer.Elapsed += Timer_Elapsed;

次に、Intervalが適切か確認します。

C#
timer.Interval = 1000;

次に、AutoResetの設定を確認します。

C#
timer.AutoReset = true;

最後に、アプリケーションが終了していないか、Timerインスタンスが破棄されていないかを確認します。

C#
Console.ReadLine();

コンソールアプリでは、Mainメソッドが終了するとTimerも動作し続けられません。

9-3. 重複する場合の確認手順

Elapsedイベントが重複して実行される場合は、まずイベント登録の回数を確認します。

C#
timer.Elapsed += Timer_Elapsed;

このコードが複数回実行されていないか確認してください。

次に、Timerインスタンスが複数作られていないか確認します。

C#
new System.Timers.Timer()

この記述が複数箇所にある場合は注意が必要です。

次に、処理時間がIntervalより長くないか確認します。

C#
timer.Interval = 1000;
// 処理に3秒かかると重複の可能性あり

長時間処理がある場合は、AutoReset=falseStop()SemaphoreSlimなどで多重実行対策を行います。

9-4. デバッグログで原因を特定する方法

原因がわからない場合は、Elapsedイベント内にログを出力します。

C#
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
Console.WriteLine($"Elapsed発生: {DateTime.Now:HH:mm:ss.fff}");
Console.WriteLine($"ThreadId: {Thread.CurrentThread.ManagedThreadId}");

try
{
Console.WriteLine("処理開始");
DoWork();
Console.WriteLine("処理終了");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}

ログに次の情報を出すと、原因を特定しやすくなります。

・Elapsedが発生した時刻
・処理開始時刻
・処理終了時刻
・スレッドID
・例外内容
・TimerのStart/Stopタイミング

重複実行が疑われる場合は、処理開始と終了のログを必ず出しましょう。

9-5. よくあるミスと修正例

よくあるミスのひとつは、ボタンを押すたびにイベント登録してしまうことです。

C#
private void buttonStart_Click(object sender, EventArgs e)
{
_timer.Elapsed += Timer_Elapsed;
_timer.Start();
}

修正例は次のとおりです。

C#
private void Form1_Load(object sender, EventArgs e)
{
_timer.Elapsed += Timer_Elapsed;
}

private void buttonStart_Click(object sender, EventArgs e)
{
_timer.Start();
}

もうひとつのミスは、Timerをローカル変数で作成して保持しないことです。

C#
void StartTimer()
{
var timer = new System.Timers.Timer(1000);
timer.Elapsed += Timer_Elapsed;
timer.Start();
}

修正例は次のとおりです。

C#
private readonly System.Timers.Timer _timer = new System.Timers.Timer(1000);

void StartTimer()
{
_timer.Elapsed -= Timer_Elapsed;
_timer.Elapsed += Timer_Elapsed;
_timer.Start();
}

さらに、長時間処理で重複してしまうケースもよくあります。

C#
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
Thread.Sleep(5000);
}

修正例は次のとおりです。

C#
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
_timer.Stop();

try
{
Thread.Sleep(5000);
}
finally
{
_timer.Start();
}
}

10. C# Timer Elapsedイベントを安全に使うためのベストプラクティス

10-1. Timerのライフサイクルを明確にする

Timerは、いつ作成し、いつ開始し、いつ停止し、いつ破棄するかを明確にすることが重要です。

おすすめは、クラスのフィールドとしてTimerを保持し、コンストラクタでイベント登録、Startメソッドで開始、Disposeで解放する形です。

C#
public class TimerService : IDisposable
{
private readonly System.Timers.Timer _timer;

public TimerService()
{
_timer = new System.Timers.Timer(1000);
_timer.AutoReset = true;
_timer.Elapsed += Timer_Elapsed;
}

public void Start()
{
_timer.Start();
}

public void Stop()
{
_timer.Stop();
}

private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
Console.WriteLine("実行");
}

public void Dispose()
{
_timer.Stop();
_timer.Elapsed -= Timer_Elapsed;
_timer.Dispose();
}
}

このように管理すると、イベントの重複登録やDispose漏れを防ぎやすくなります。

10-2. Disposeでリソースを解放する

Timerを使い終わったら、必ずDispose()を呼び出します。

C#
_timer.Dispose();

イベントハンドラも解除しておくと、参照が残るリスクを減らせます。

C#
_timer.Elapsed -= Timer_Elapsed;
_timer.Dispose();

アプリケーション終了時、フォームのクローズ時、サービス停止時などに確実に解放しましょう。

10-3. 例外処理を必ず入れる

Elapsedイベント内では、例外処理を必ず入れるべきです。

C#
private void Timer_Elapsed(object? sender, ElapsedEventArgs e)
{
try
{
DoWork();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}

Timerの処理はバックグラウンドで動くことが多いため、例外が見落とされやすいです。

ログ出力を行い、問題が発生したときに原因を追跡できるようにしましょう。

10-4. 長時間処理は多重実行対策を行う

Elapsedイベント内の処理がIntervalより長くなる可能性がある場合は、多重実行対策が必要です。

代表的な対策は次のとおりです。

・AutoReset=falseにする
・Elapsed開始時にStopする
・処理完了後にStartする
・Monitor.TryEnterを使う
・SemaphoreSlimを使う
・処理中フラグを使う

特に、API呼び出し、ファイル処理、DB更新、外部サービス連携などは処理時間が変動しやすいため、重複実行を前提に設計することが重要です。

10-5. 用途に合ったTimerを選ぶ

C#には複数のTimerがあります。

System.Timers.Timerは便利ですが、すべての用途に最適というわけではありません。

UI更新なら、Windows FormsではSystem.Windows.Forms.Timer、WPFではDispatcherTimerを使う方が安全です。

非同期の定期処理なら、.NET 6以降ではPeriodicTimerも有力です。

Elapsedイベントを使う場合は、次の点を意識しましょう。

・Elapsedは別スレッドで実行される
・UI更新は直接行わない
・イベント登録は1回だけにする
・Timerインスタンスを保持する
・長時間処理では重複実行を防ぐ
・例外処理を入れる
・使い終わったらDisposeする

まとめ

C#のTimerElapsedイベントを使うと、一定間隔の処理を簡単に実行できます。

しかし、Elapsedイベントが動かない、1回しか実行されない、重複して実行される、UI更新でエラーになるといったトラブルも起こりやすいです。

Elapsedイベントが動かない場合は、まずStart()またはEnabled=trueを設定しているか、Elapsedイベントを登録しているか、Timerインスタンスを保持しているかを確認しましょう。

重複して実行される場合は、イベントハンドラを複数回登録していないか、処理時間がIntervalより長くないか、Timerインスタンスを複数生成していないかを確認することが重要です。

安全に使うためには、イベント登録を1回だけにし、長時間処理ではAutoReset=falseStop()SemaphoreSlimMonitor.TryEnterなどを使って多重実行を防ぎます。

また、System.Timers.TimerElapsedイベントは通常別スレッドで実行されるため、Windows FormsやWPFでUIを更新する場合はInvokeDispatcherを使う必要があります。

C#のTimerには複数の種類があるため、バックグラウンド処理ならSystem.Timers.Timer、UI更新ならSystem.Windows.Forms.TimerDispatcherTimer、非同期処理ならPeriodicTimerというように、用途に合ったTimerを選ぶことも大切です。

C# Timer Elapsedを正しく使うには、単にIntervalElapsedを設定するだけでなく、ライフサイクル、スレッド、例外処理、重複実行対策まで含めて設計することが重要です。