C#タイマーの使い方完全ガイド|Timer・DispatcherTimer・Task.Delayの違いと実装例

はじめに

C#で「一定時間ごとに処理したい」「5秒後に1回だけ実行したい」「画面の時計表示を1秒ごとに更新したい」と考えたとき、多くの場合に使うのがタイマー処理です。

ただし、C#には名前の似たタイマーが複数あります。代表的なものだけでも、System.Threading.TimerSystem.Timers.Timer、WPFのDispatcherTimer、WinFormsのTimer、そしてタイマーの代わりによく使われるTask.Delayがあります。

どれも「時間を扱う」点では似ていますが、実行されるスレッド、UI更新との相性、非同期処理との組み合わせやすさ、停止や破棄の方法が異なります。選び方を間違えると、UI更新時に例外が出たり、処理が重複したり、タイマーが停止しなかったりする原因になります。

この記事では、C#タイマーの基本から、TimerDispatcherTimerTask.Delayの違い、実装例、よくあるエラーと対処法までをまとめて解説します。

1. C#タイマーでできることと検索ユーザーが知りたいポイント

1-1. C#でタイマー処理が必要になる主なケース

C#のタイマー処理は、次のような場面でよく使われます。

やりたいこと
一定間隔で処理する1秒ごとに時刻を表示する
指定時間後に実行する5秒後にメッセージを表示する
定期的に監視する30秒ごとにAPIやファイルを確認する
タイムアウトを実装する10秒以内に応答がなければキャンセルする
UIを更新するWPFやWinFormsで画面の表示を更新する
バックグラウンド処理を動かすサーバー処理やログ監視を定期実行する

同じ「タイマー」といっても、コンソールアプリ、WPF、WinForms、ASP.NET、Windowsサービスなど、アプリケーションの種類によって適した実装が変わります。

1-2. 「一定間隔で実行」「指定時間後に実行」「UI更新」の違い

C#タイマーを選ぶときは、まず目的を分けて考えることが重要です。

「一定間隔で実行」は、1秒ごと、10秒ごと、1分ごとなど、繰り返し処理したい場合です。たとえば、定期的なログ出力、ファイル監視、サーバー状態チェックなどが該当します。

「指定時間後に実行」は、3秒後に1回だけ実行する、一定時間待ってから次の処理に進む、といった場合です。この用途ではTask.DelaySystem.Threading.Timerの1回実行が使いやすいです。

「UI更新」は、WPFやWinFormsで画面上のラベル、テキストボックス、プログレスバーなどを更新する場合です。UI部品は基本的にUIスレッドから操作する必要があるため、バックグラウンドスレッドで動くタイマーをそのまま使うと例外が発生することがあります。

1-3. C#のTimerが複数あって迷いやすい理由

C#でタイマーが分かりにくい最大の理由は、Timerという名前のクラスが複数あることです。

たとえば、次のように複数の名前空間にTimerが存在します。

C#
System.Threading.Timer
System.Timers.Timer
System.Windows.Forms.Timer
System.Windows.Threading.DispatcherTimer

using System.Timers;using System.Threading;を同時に書いていると、Timerという型名だけではどちらのタイマーか分からなくなる場合もあります。

そのため、実務では次のように完全修飾名を書くか、エイリアスを使うと読みやすくなります。

C#
using ThreadingTimer = System.Threading.Timer;
using TimersTimer = System.Timers.Timer;

1-4. この記事で扱うTimer・DispatcherTimer・Task.Delayの範囲

この記事では、C#でよく使う次の4つを中心に解説します。

種類主な用途
System.Threading.Timer軽量なバックグラウンド定期処理
System.Timers.Timerイベントベースの定期処理
DispatcherTimerWPFのUI更新
Task.Delay非同期処理での待機・繰り返し処理

WinFormsのSystem.Windows.Forms.Timerについても比較として触れますが、中心はTimerDispatcherTimerTask.Delayです。

2. C#で使える主なタイマーの種類

2-1. System.Threading.Timerとは

System.Threading.Timerは、指定した時間後または一定間隔ごとにコールバックメソッドを実行するタイマーです。処理はThreadPool上で実行されます。

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

項目内容
名前空間System.Threading
実行方法コールバック
実行スレッドThreadPool
UI更新直接は不向き
用途軽量なバックグラウンド処理

System.Threading.Timerはシンプルで軽量ですが、イベントではなくコールバックで処理を書くため、慣れていないと少し分かりにくい場合があります。

2-2. System.Timers.Timerとは

System.Timers.Timerは、指定した間隔が経過したときにElapsedイベントを発生させるタイマーです。繰り返し実行するか、1回だけ実行するかをAutoResetで切り替えられます。

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

項目内容
名前空間System.Timers
実行方法Elapsedイベント
実行スレッド主にThreadPool
UI更新直接は注意が必要
用途サーバー処理、サービス、定期処理

イベントベースで書けるため、System.Threading.Timerより読みやすいと感じる人も多いです。

2-3. DispatcherTimerとは

DispatcherTimerは、WPFでよく使われるタイマーです。Dispatcherのキューに統合され、UIスレッド上でTickイベントが実行されます。

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

項目内容
名前空間System.Windows.Threading
実行方法Tickイベント
実行スレッドUIスレッド
UI更新向いている
用途WPF画面の定期更新

WPFで画面表示を定期更新する場合は、まずDispatcherTimerを検討するとよいでしょう。

2-4. Task.Delayとは

Task.Delayは、指定した時間だけ非同期に待機するためのメソッドです。タイマー専用クラスではありませんが、async/awaitと組み合わせることで、指定時間後の処理や一定間隔のループ処理を簡潔に書けます。

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

項目内容
名前空間System.Threading.Tasks
実行方法await Task.Delay(...)
実行スレッド待機中にスレッドを占有しない
UI更新await後のコンテキスト次第
用途非同期処理、待機、タイムアウト

現在のC#では、非同期処理と相性がよいため、単純な待機やキャンセル可能なループではTask.Delayが使われることも多いです。

2-5. WinFormsのTimerとの違い

WinFormsにはSystem.Windows.Forms.Timerがあります。これはWindows FormsアプリのUIスレッド上でTickイベントを発生させるタイマーです。

WPFのDispatcherTimerと似ており、画面部品を直接更新しやすい一方で、重い処理を実行するとUIが固まります。

種類主な対象
DispatcherTimerWPF
System.Windows.Forms.TimerWinForms
System.Threading.Timerバックグラウンド処理
System.Timers.Timerサーバー・サービス系処理
Task.Delay非同期待機・非同期ループ

2-6. どのタイマーを選ぶべきか早見表

用途おすすめ
WPFで画面を更新したいDispatcherTimer
WinFormsで画面を更新したいSystem.Windows.Forms.Timer
バックグラウンドで軽く定期実行したいSystem.Threading.Timer
イベント形式で定期実行したいSystem.Timers.Timer
async/awaitで待機したいTask.Delay
キャンセル可能な非同期ループを書きたいTask.Delay + CancellationToken
5秒後に1回だけ実行したいTask.DelayまたはSystem.Threading.Timer
UIに関係ないサーバー処理System.Timers.TimerまたはSystem.Threading.Timer

3. System.Threading.Timerの使い方と実装例

3-1. System.Threading.Timerの基本構文

System.Threading.Timerの基本構文は次のとおりです。

C#
using System;
using System.Threading;

class Program
{
private static Timer? _timer;

static void Main()
{
_timer = new Timer(
callback: TimerCallback,
state: null,
dueTime: 1000,
period: 1000);

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

_timer.Dispose();
}

private static void TimerCallback(object? state)
{
Console.WriteLine($"実行時刻: {DateTime.Now:HH:mm:ss}");
}
}

引数の意味は次のとおりです。

引数意味
callback実行するメソッド
stateコールバックに渡す任意のデータ
dueTime最初に実行するまでの待ち時間
period2回目以降の実行間隔

上の例では、1秒後に初回実行し、その後1秒ごとに処理を繰り返します。

3-2. 一定間隔で処理を繰り返すサンプル

3秒ごとに処理を繰り返す例です。

C#
using System;
using System.Threading;

class Program
{
private static Timer? _timer;

static void Main()
{
_timer = new Timer(
_ => DoWork(),
null,
TimeSpan.Zero,
TimeSpan.FromSeconds(3));

Console.WriteLine("3秒ごとに処理します。Enterキーで終了。");
Console.ReadLine();

_timer.Dispose();
}

private static void DoWork()
{
Console.WriteLine($"処理実行: {DateTime.Now:HH:mm:ss}");
}
}

TimeSpan.Zeroを指定すると、タイマー作成後すぐに初回処理が実行されます。

3-3. 指定時間後に1回だけ処理を実行する方法

1回だけ実行したい場合は、periodTimeout.Infiniteを指定します。

C#
using System;
using System.Threading;

class Program
{
private static Timer? _timer;

static void Main()
{
_timer = new Timer(
_ => Console.WriteLine("5秒後に1回だけ実行されました。"),
null,
TimeSpan.FromSeconds(5),
Timeout.InfiniteTimeSpan);

Console.WriteLine("待機中...");
Console.ReadLine();

_timer.Dispose();
}
}

この例では、5秒後に1回だけコールバックが実行され、その後は繰り返しません。

3-4. Changeメソッドで開始・停止・間隔変更を行う方法

System.Threading.Timerは、Changeメソッドで開始、停止、間隔変更ができます。

C#
using System;
using System.Threading;

class Program
{
private static Timer? _timer;

static void Main()
{
_timer = new Timer(_ => PrintTime(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);

Console.WriteLine("開始します。");
_timer.Change(TimeSpan.Zero, TimeSpan.FromSeconds(1));

Thread.Sleep(5000);

Console.WriteLine("停止します。");
_timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);

Thread.Sleep(3000);

Console.WriteLine("2秒間隔で再開します。");
_timer.Change(TimeSpan.Zero, TimeSpan.FromSeconds(2));

Console.ReadLine();
_timer.Dispose();
}

private static void PrintTime()
{
Console.WriteLine(DateTime.Now.ToString("HH:mm:ss"));
}
}

停止するときは、dueTimeperiodの両方にTimeout.InfiniteTimeSpanを指定します。

3-5. Disposeでタイマーを正しく破棄する方法

タイマーが不要になったらDisposeで破棄します。

C#
_timer?.Dispose();
_timer = null;

System.Threading.TimerIDisposableを実装しているため、使い終わったら明示的に破棄することが大切です。破棄しないと、不要なコールバックが残り続ける原因になります。

クラス内で管理する場合は、次のようにIDisposableを実装すると安全です。

C#
using System;
using System.Threading;

public class Worker : IDisposable
{
private readonly Timer _timer;
private bool _disposed;

public Worker()
{
_timer = new Timer(_ => DoWork(), null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
}

private void DoWork()
{
if (_disposed) return;

Console.WriteLine("処理中...");
}

public void Dispose()
{
if (_disposed) return;

_timer.Dispose();
_disposed = true;
}
}

3-6. System.Threading.Timerを使うときの注意点

System.Threading.Timerを使うときは、次の点に注意します。

1つ目は、コールバックがThreadPoolで実行されることです。WPFやWinFormsのUI部品を直接更新してはいけません。

2つ目は、処理が重いと重複実行される可能性があることです。タイマー間隔より処理時間が長い場合、前回の処理が終わる前に次のコールバックが呼ばれる場合があります。

3つ目は、タイマーへの参照を保持することです。参照がなくなるとガベージコレクションの対象になる可能性があります。

重複実行を防ぎたい場合は、次のようにフラグを使います。

C#
using System;
using System.Threading;

class Program
{
private static Timer? _timer;
private static int _running = 0;

static void Main()
{
_timer = new Timer(_ => DoWork(), null, TimeSpan.Zero, TimeSpan.FromSeconds(1));

Console.ReadLine();
_timer.Dispose();
}

private static void DoWork()
{
if (Interlocked.Exchange(ref _running, 1) == 1)
{
return;
}

try
{
Console.WriteLine("処理開始");
Thread.Sleep(3000);
Console.WriteLine("処理終了");
}
finally
{
Interlocked.Exchange(ref _running, 0);
}
}
}

4. System.Timers.Timerの使い方と実装例

4-1. System.Timers.Timerの基本構文

System.Timers.Timerは、Elapsedイベントに処理を書きます。

C#
using System;
using System.Timers;

class Program
{
private static System.Timers.Timer? _timer;

static void Main()
{
_timer = new System.Timers.Timer(1000);
_timer.Elapsed += OnElapsed;
_timer.AutoReset = true;
_timer.Start();

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

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

private static void OnElapsed(object? sender, ElapsedEventArgs e)
{
Console.WriteLine($"実行時刻: {e.SignalTime:HH:mm:ss}");
}
}

Intervalの単位はミリ秒です。1000を指定すると1秒間隔になります。

4-2. Elapsedイベントで定期実行するサンプル

2秒ごとに定期処理を実行する例です。

C#
using System;
using System.Timers;

class Program
{
private static System.Timers.Timer? _timer;

static void Main()
{
_timer = new System.Timers.Timer
{
Interval = 2000,
AutoReset = true,
Enabled = true
};

_timer.Elapsed += (_, e) =>
{
Console.WriteLine($"2秒ごとに実行: {e.SignalTime:HH:mm:ss}");
};

Console.ReadLine();

_timer.Dispose();
}
}

Enabled = trueにするとタイマーが開始されます。Start()を呼び出しても同じように開始できます。

4-3. AutoResetで繰り返し実行と1回実行を切り替える方法

AutoResetは、タイマーを繰り返すかどうかを指定するプロパティです。

C#
_timer.AutoReset = true;  // 繰り返し実行
_timer.AutoReset = false; // 1回だけ実行

5秒後に1回だけ実行する例は次のとおりです。

C#
using System;
using System.Timers;

class Program
{
private static System.Timers.Timer? _timer;

static void Main()
{
_timer = new System.Timers.Timer(5000);
_timer.AutoReset = false;
_timer.Elapsed += (_, _) =>
{
Console.WriteLine("5秒後に1回だけ実行されました。");
};

_timer.Start();

Console.ReadLine();
_timer.Dispose();
}
}

4-4. Start・Stopでタイマーを制御する方法

System.Timers.Timerは、StartStopで制御できます。

C#
_timer.Start(); // 開始
_timer.Stop(); // 停止

ボタンやコマンドのような操作に合わせて開始・停止したい場合、System.Threading.TimerChangeより直感的に扱えます。

C#
using System;
using System.Timers;

class Program
{
private static readonly System.Timers.Timer Timer = new(1000);

static void Main()
{
Timer.Elapsed += (_, _) => Console.WriteLine(DateTime.Now.ToString("HH:mm:ss"));

Console.WriteLine("S:開始 / T:停止 / Q:終了");

while (true)
{
var key = Console.ReadKey(true).Key;

if (key == ConsoleKey.S)
{
Timer.Start();
Console.WriteLine("開始しました。");
}
else if (key == ConsoleKey.T)
{
Timer.Stop();
Console.WriteLine("停止しました。");
}
else if (key == ConsoleKey.Q)
{
break;
}
}

Timer.Dispose();
}
}

4-5. 例外処理とイベント内処理の注意点

Elapsedイベント内では、例外処理を明示的に書くのが安全です。

C#
private static void OnElapsed(object? sender, ElapsedEventArgs e)
{
try
{
Console.WriteLine("定期処理を実行します。");

// ここに処理を書く
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}
}

特にasyncを使う場合は注意が必要です。Elapsedイベントでasync voidのような形になると、例外の扱いが難しくなります。非同期処理を行う場合は、内部で必ずtry-catchを入れましょう。

C#
_timer.Elapsed += async (_, _) =>
{
try
{
await DoAsync();
}
catch (Exception ex)
{
Console.WriteLine($"非同期処理エラー: {ex.Message}");
}
};

static async Task DoAsync()
{
await Task.Delay(500);
Console.WriteLine("非同期処理完了");
}

また、処理時間がIntervalより長い場合、次のElapsedが重なって発生する可能性があります。重複を避けたい場合は、実行中フラグやSemaphoreSlimを使います。

4-6. System.Threading.Timerとの違い

System.Threading.TimerSystem.Timers.Timerの大きな違いは、書き方と制御方法です。

比較項目System.Threading.TimerSystem.Timers.Timer
書き方コールバックElapsedイベント
開始・停止ChangeStart / Stop
1回実行Timeout.InfiniteAutoReset = false
実行スレッドThreadPool主にThreadPool
向いている用途軽量処理イベント形式の定期処理

どちらもUIスレッドで実行されるわけではないため、WPFやWinFormsの画面部品を直接更新する用途には向きません。

5. DispatcherTimerの使い方と実装例

5-1. DispatcherTimerとは

DispatcherTimerは、WPFアプリで使われるタイマーです。TickイベントがUIスレッド上で実行されるため、ラベルやテキストボックスなどのUI部品を直接更新しやすいのが特徴です。

WPFで「1秒ごとに現在時刻を表示する」「カウントダウンを表示する」「画面の状態を定期的に更新する」といった用途に向いています。

5-2. WPFで画面表示を定期更新するサンプル

WPFのMainWindow.xamlに次のようなTextBlockがあるとします。

XML
<Window x:Class="TimerSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="TimerSample" Height="200" Width="300">
<Grid>
<TextBlock x:Name="ClockText"
FontSize="32"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</Window>

コードビハインドでは、DispatcherTimerを使って1秒ごとに表示を更新します。

C#
using System;
using System.Windows;
using System.Windows.Threading;

namespace TimerSample
{
public partial class MainWindow : Window
{
private readonly DispatcherTimer _timer = new();

public MainWindow()
{
InitializeComponent();

_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += Timer_Tick;
_timer.Start();
}

private void Timer_Tick(object? sender, EventArgs e)
{
ClockText.Text = DateTime.Now.ToString("HH:mm:ss");
}
}
}

TickイベントはUIスレッド上で動作するため、ClockText.Textを直接更新できます。

5-3. Tickイベントで処理を実行する方法

DispatcherTimerの基本は、Intervalを設定し、Tickイベントに処理を書き、Startで開始する流れです。

C#
var timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(1);
timer.Tick += (sender, e) =>
{
// 定期的に実行したい処理
};
timer.Start();

停止するときはStopを呼びます。

C#
timer.Stop();

5-4. UIスレッドで動作するメリットと注意点

DispatcherTimerの最大のメリットは、UI更新が簡単なことです。

通常、バックグラウンドスレッドからWPFのUI部品を操作すると、スレッドが異なるため例外が発生します。しかし、DispatcherTimerはUIスレッド上で実行されるため、画面部品を直接操作できます。

一方で、UIスレッドで動くということは、重い処理を書くと画面が固まるということでもあります。

C#
private void Timer_Tick(object? sender, EventArgs e)
{
// 重い処理をここに直接書くのは避ける
Thread.Sleep(5000); // UIが固まる原因
}

5-5. 重い処理をDispatcherTimerで実行してはいけない理由

DispatcherTimerTickイベント内で時間のかかる処理を直接実行すると、UIスレッドが占有されます。その結果、ボタンが反応しない、画面が再描画されない、アプリがフリーズしたように見える、といった問題が発生します。

悪い例は次のとおりです。

C#
private void Timer_Tick(object? sender, EventArgs e)
{
// API呼び出しや大量ファイル処理などを直接実行するのは避ける
var result = HeavyProcess();
ResultText.Text = result;
}

重い処理はTask.Runなどでバックグラウンドに逃がし、UI更新だけをUIスレッドで行うのが基本です。

5-6. DispatcherTimerと非同期処理を組み合わせる方法

DispatcherTimerで非同期処理を行う場合は、重複実行に注意します。async処理が終わる前に次のTickが発生すると、同じ処理が並行して走る可能性があります。

C#
private bool _isRunning;

private async void Timer_Tick(object? sender, EventArgs e)
{
if (_isRunning) return;

_isRunning = true;

try
{
StatusText.Text = "処理中...";

var result = await Task.Run(() =>
{
Thread.Sleep(2000);
return "完了";
});

StatusText.Text = result;
}
catch (Exception ex)
{
StatusText.Text = $"エラー: {ex.Message}";
}
finally
{
_isRunning = false;
}
}

このように_isRunningフラグを使うと、前回処理が終わっていない間は次の処理をスキップできます。

6. Task.Delayの使い方と実装例

6-1. Task.Delayとは

Task.Delayは、指定した時間だけ非同期に待機するメソッドです。Thread.Sleepと違い、待機中にスレッドをブロックしないため、async/awaitを使う現代的なC#コードと相性がよいです。

たとえば、3秒待ってから処理する場合は次のように書きます。

C#
await Task.Delay(3000);

または、TimeSpanを使って次のように書けます。

C#
await Task.Delay(TimeSpan.FromSeconds(3));

6-2. async/awaitで指定時間待機する基本例

基本的な例です。

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

class Program
{
static async Task Main()
{
Console.WriteLine("開始");

await Task.Delay(TimeSpan.FromSeconds(3));

Console.WriteLine("3秒後に実行");
}
}

await Task.Delay(...)は「指定時間が経過するまで非同期で待つ」という意味です。待機中に現在のスレッドを占有しないため、UIアプリやサーバーアプリでも使いやすいです。

6-3. while文とTask.Delayで一定間隔処理を行う方法

while文とTask.Delayを組み合わせると、一定間隔で処理できます。

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

class Program
{
static async Task Main()
{
using var cts = new CancellationTokenSource();

var task = RunPeriodicAsync(cts.Token);

Console.WriteLine("Enterキーで停止します。");
Console.ReadLine();

cts.Cancel();

await task;
}

static async Task RunPeriodicAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
Console.WriteLine($"実行: {DateTime.Now:HH:mm:ss}");

await Task.Delay(TimeSpan.FromSeconds(1), token);
}
}
}

ただし、このコードはキャンセル時にTaskCanceledExceptionが発生する可能性があります。実務では次のように例外処理を入れると安全です。

C#
static async Task RunPeriodicAsync(CancellationToken token)
{
try
{
while (!token.IsCancellationRequested)
{
Console.WriteLine($"実行: {DateTime.Now:HH:mm:ss}");

await Task.Delay(TimeSpan.FromSeconds(1), token);
}
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました。");
}
}

6-4. CancellationTokenで待機処理をキャンセルする方法

Task.DelayCancellationTokenを受け取れます。これにより、待機中の処理を途中でキャンセルできます。

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

class Program
{
static async Task Main()
{
using var cts = new CancellationTokenSource();

var task = WaitAsync(cts.Token);

await Task.Delay(2000);

cts.Cancel();

await task;
}

static async Task WaitAsync(CancellationToken token)
{
try
{
Console.WriteLine("10秒待機を開始します。");

await Task.Delay(TimeSpan.FromSeconds(10), token);

Console.WriteLine("10秒経過しました。");
}
catch (OperationCanceledException)
{
Console.WriteLine("待機がキャンセルされました。");
}
}
}

この例では、本来10秒待つところを2秒後にキャンセルしています。

6-5. Thread.Sleepとの違い

Task.DelayThread.Sleepは似ていますが、動作は大きく異なります。

比較項目Task.DelayThread.Sleep
待機方法非同期で待機スレッドを停止
スレッド占有しないする
UIアプリとの相性よい悪い
キャンセルCancellationTokenで可能基本的に不可
async/await相性がよい相性が悪い

UIアプリでThread.Sleepを使うと画面が固まる原因になります。非同期コードでは、基本的にTask.Delayを使うのがおすすめです。

悪い例です。

C#
private void Button_Click(object sender, EventArgs e)
{
Thread.Sleep(3000); // UIが固まる
}

良い例です。

C#
private async void Button_Click(object sender, EventArgs e)
{
await Task.Delay(3000); // UIを固めにくい
}

6-6. Task.Delayをタイマー代わりに使うときの注意点

Task.Delayをタイマー代わりに使う場合は、次の点に注意します。

1つ目は、処理時間を含めた間隔になることです。

C#
while (true)
{
await DoWorkAsync(); // 2秒かかる
await Task.Delay(1000); // 1秒待つ
}

この場合、実行間隔は約1秒ではなく、「処理時間 + 1秒」になります。

2つ目は、キャンセル手段を用意することです。無限ループでTask.Delayを使う場合は、必ずCancellationTokenを使いましょう。

3つ目は、例外処理を書くことです。ループ内で例外が発生すると、定期処理全体が止まる可能性があります。

C#
while (!token.IsCancellationRequested)
{
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}

await Task.Delay(TimeSpan.FromSeconds(5), token);
}

7. Timer・DispatcherTimer・Task.Delayの違い

7-1. 実行スレッドの違い

タイマー選びで最も重要なのが、どのスレッドで処理が実行されるかです。

種類実行スレッド
System.Threading.TimerThreadPool
System.Timers.Timer主にThreadPool
DispatcherTimerWPFのUIスレッド
System.Windows.Forms.TimerWinFormsのUIスレッド
Task.Delay待機後の継続処理はコンテキスト次第

UI更新が必要なら、UIスレッドで動くDispatcherTimerやWinFormsのTimerが扱いやすいです。バックグラウンド処理なら、System.Threading.TimerSystem.Timers.Timerが向いています。

7-2. UI更新に向いているかの違い

UI更新に向いているかどうかを比較すると、次のようになります。

種類UI更新
DispatcherTimerWPFで向いている
System.Windows.Forms.TimerWinFormsで向いている
System.Threading.Timer直接更新は不可
System.Timers.Timer直接更新は注意
Task.DelayUIイベント内のawaitなら扱いやすい

WPFでSystem.Threading.TimerからUIを更新したい場合は、Dispatcher.InvokeDispatcher.BeginInvokeを使います。

C#
Application.Current.Dispatcher.Invoke(() =>
{
ClockText.Text = DateTime.Now.ToString("HH:mm:ss");
});

ただし、画面更新が目的なら最初からDispatcherTimerを選ぶ方が簡単です。

7-3. 定期実行と待機処理の違い

Timer系は「一定間隔で呼び出す」ことが主目的です。一方、Task.Delayは「一定時間待つ」ことが主目的です。

目的向いている方法
1秒ごとに処理したいTimer系またはTask.Delayループ
5秒だけ待ちたいTask.Delay
5秒後に1回実行したいTask.Delayまたは1回実行Timer
UIを1秒ごとに更新したいDispatcherTimer
非同期処理を繰り返したいTask.Delay + async/await

7-4. 非同期処理との相性の違い

非同期処理との相性は、Task.Delayが最もよいです。

C#
while (!token.IsCancellationRequested)
{
await CheckApiAsync();
await Task.Delay(TimeSpan.FromSeconds(10), token);
}

System.Timers.TimerDispatcherTimerでもasyncは使えますが、イベントハンドラーがasync voidになりやすいため、例外処理や重複実行に注意が必要です。

C#
private async void Timer_Tick(object? sender, EventArgs e)
{
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}

7-5. 精度・遅延・処理の重複に関する違い

C#のタイマーは、リアルタイム制御や高精度な計測に向いているわけではありません。OSのスケジューリング、ThreadPoolの混雑、UIスレッドの負荷、処理時間などによって遅延することがあります。

特に次の点に注意します。

問題原因
実行が遅れるスレッドが空いていない、UIが忙しい
間隔がずれる処理時間が長い
重複実行される前回処理中に次のタイマーが発火
UIが固まるUIスレッドで重い処理をしている
停止後に処理が走るすでにキューに入った処理が実行される

高精度な時間計測にはStopwatchを使い、タイマーは「おおよその間隔で処理を起動するもの」と考えるのが安全です。

7-6. 用途別おすすめの選び方

用途別にまとめると、次のようになります。

用途おすすめ
WPFの時計表示DispatcherTimer
WinFormsの画面更新System.Windows.Forms.Timer
コンソールで定期実行System.Threading.TimerまたはTask.Delay
サーバーで定期チェックSystem.Timers.TimerまたはTask.Delay
非同期APIを定期実行Task.Delay
1回だけ遅延実行Task.Delay
軽量なバックグラウンド定期処理System.Threading.Timer
イベントベースで書きたいSystem.Timers.Timer

迷った場合は、UI更新ならDispatcherTimer、非同期処理ならTask.Delay、バックグラウンドの軽量処理ならSystem.Threading.Timerを選ぶとよいでしょう。

8. よくある実装パターン別C#タイマーサンプル

8-1. 1秒ごとに現在時刻を表示する

コンソールアプリで1秒ごとに現在時刻を表示する例です。

C#
using System;
using System.Threading;

class Program
{
private static Timer? _timer;

static void Main()
{
_timer = new Timer(
_ => Console.WriteLine(DateTime.Now.ToString("HH:mm:ss")),
null,
TimeSpan.Zero,
TimeSpan.FromSeconds(1));

Console.ReadLine();

_timer.Dispose();
}
}

WPFで画面に表示するならDispatcherTimerを使います。

C#
private readonly DispatcherTimer _timer = new();

public MainWindow()
{
InitializeComponent();

_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += (_, _) =>
{
ClockText.Text = DateTime.Now.ToString("HH:mm:ss");
};
_timer.Start();
}

8-2. 5秒後に処理を1回だけ実行する

Task.Delayを使うと簡単です。

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

class Program
{
static async Task Main()
{
Console.WriteLine("5秒待ちます。");

await Task.Delay(TimeSpan.FromSeconds(5));

Console.WriteLine("5秒後に実行されました。");
}
}

System.Threading.Timerで書く場合は次のようになります。

C#
using System;
using System.Threading;

class Program
{
private static Timer? _timer;

static void Main()
{
_timer = new Timer(
_ => Console.WriteLine("5秒後に1回だけ実行"),
null,
TimeSpan.FromSeconds(5),
Timeout.InfiniteTimeSpan);

Console.ReadLine();

_timer.Dispose();
}
}

8-3. ボタンクリックでタイマーを開始・停止する

WPFでボタンクリックにより開始・停止する例です。

C#
private readonly DispatcherTimer _timer = new();

public MainWindow()
{
InitializeComponent();

_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += (_, _) =>
{
ClockText.Text = DateTime.Now.ToString("HH:mm:ss");
};
}

private void StartButton_Click(object sender, RoutedEventArgs e)
{
_timer.Start();
}

private void StopButton_Click(object sender, RoutedEventArgs e)
{
_timer.Stop();
}

コンソールアプリでSystem.Timers.Timerを使う場合は、StartStopを呼び分けます。

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

8-4. 一定時間ごとにAPIやファイルをチェックする

非同期APIを定期的に呼び出すなら、Task.DelayCancellationTokenの組み合わせが扱いやすいです。

C#
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

class Program
{
private static readonly HttpClient Client = new();

static async Task Main()
{
using var cts = new CancellationTokenSource();

var task = CheckApiLoopAsync(cts.Token);

Console.WriteLine("Enterキーで停止します。");
Console.ReadLine();

cts.Cancel();
await task;
}

static async Task CheckApiLoopAsync(CancellationToken token)
{
try
{
while (!token.IsCancellationRequested)
{
try
{
var response = await Client.GetAsync("https://example.com", token);
Console.WriteLine($"ステータス: {response.StatusCode}");
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
Console.WriteLine($"APIチェックエラー: {ex.Message}");
}

await Task.Delay(TimeSpan.FromSeconds(30), token);
}
}
catch (OperationCanceledException)
{
Console.WriteLine("APIチェックを停止しました。");
}
}
}

ファイルの存在を定期的に確認する場合も同じ考え方です。

C#
static async Task WatchFileAsync(string path, CancellationToken token)
{
while (!token.IsCancellationRequested)
{
if (File.Exists(path))
{
Console.WriteLine("ファイルが見つかりました。");
}

await Task.Delay(TimeSpan.FromSeconds(5), token);
}
}

8-5. タイムアウト処理を実装する

タイムアウト処理には、CancellationTokenSource.CancelAfterが便利です。

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

class Program
{
static async Task Main()
{
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5));

try
{
await LongRunningAsync(cts.Token);
Console.WriteLine("処理が完了しました。");
}
catch (OperationCanceledException)
{
Console.WriteLine("タイムアウトしました。");
}
}

static async Task LongRunningAsync(CancellationToken token)
{
await Task.Delay(TimeSpan.FromSeconds(10), token);
}
}

この例では、処理自体は10秒かかりますが、5秒でキャンセルされます。

8-6. カウントダウンタイマーを作る

WPFで10秒のカウントダウンを表示する例です。

C#
private readonly DispatcherTimer _timer = new();
private int _remainingSeconds = 10;

public MainWindow()
{
InitializeComponent();

CountText.Text = _remainingSeconds.ToString();

_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += Timer_Tick;
_timer.Start();
}

private void Timer_Tick(object? sender, EventArgs e)
{
_remainingSeconds--;

CountText.Text = _remainingSeconds.ToString();

if (_remainingSeconds <= 0)
{
_timer.Stop();
CountText.Text = "終了";
}
}

コンソールで作る場合は、Task.Delayでも書けます。

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

class Program
{
static async Task Main()
{
for (int i = 10; i >= 0; i--)
{
Console.WriteLine(i);
await Task.Delay(1000);
}

Console.WriteLine("終了");
}
}

9. C#タイマーでよくあるエラーと対処法

9-1. タイマーが動かない原因

タイマーが動かない場合、よくある原因は次のとおりです。

原因対処法
Start()を呼んでいないStart()またはEnabled = trueを設定する
dueTimeが無限になっているChangeで開始する
タイマーの参照が保持されていないフィールドに保持する
アプリがすぐ終了しているConsole.ReadLine()などで待機する
例外で処理が止まっているtry-catchを入れる

特にSystem.Threading.Timerでは、ローカル変数だけに入れていると意図せず参照がなくなる可能性があります。クラスフィールドとして保持するのが安全です。

C#
private static Timer? _timer;

9-2. UIを更新しようとして例外が出る原因

WPFやWinFormsでは、UI部品を作成したスレッド以外から直接操作すると例外が発生することがあります。

悪い例です。

C#
_timer = new Timer(_ =>
{
ClockText.Text = DateTime.Now.ToString("HH:mm:ss"); // WPFでは危険
}, null, 0, 1000);

WPFではDispatcherを使ってUIスレッドに処理を渡します。

C#
_timer = new Timer(_ =>
{
Dispatcher.Invoke(() =>
{
ClockText.Text = DateTime.Now.ToString("HH:mm:ss");
});
}, null, 0, 1000);

ただし、UI更新が目的ならDispatcherTimerを使う方が自然です。

9-3. 処理が重複して実行される原因

タイマーの間隔より処理時間が長いと、前回の処理が終わる前に次の処理が始まることがあります。

たとえば、1秒間隔のタイマーで3秒かかる処理を実行すると、複数の処理が重なりやすくなります。

対処法は、実行中フラグを使うことです。

C#
private static int _isRunning = 0;

private static void OnTimer()
{
if (Interlocked.Exchange(ref _isRunning, 1) == 1)
{
return;
}

try
{
// 時間のかかる処理
}
finally
{
Interlocked.Exchange(ref _isRunning, 0);
}
}

非同期処理ならSemaphoreSlimを使う方法もあります。

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

private async Task DoWorkAsync()
{
if (!await _semaphore.WaitAsync(0))
{
return;
}

try
{
await Task.Delay(3000);
}
finally
{
_semaphore.Release();
}
}

9-4. タイマーが停止しない原因

タイマーを停止したつもりでも処理が実行される場合、すでにキューに入ったコールバックが実行されている可能性があります。System.Timers.Timerでも、停止後にElapsedが呼ばれる場合があります。

対策として、停止フラグを用意します。

C#
private static bool _stopped;

private static void StopTimer()
{
_stopped = true;
_timer?.Stop();
}

private static void OnElapsed(object? sender, ElapsedEventArgs e)
{
if (_stopped) return;

// 処理
}

System.Threading.Timerの場合は、停止時にChangeで無限指定します。

C#
_timer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);

完全に不要になったらDisposeします。

C#
_timer.Dispose();

9-5. メモリリークを防ぐための破棄方法

タイマーはイベントやコールバックを保持するため、不要になったら停止・破棄することが重要です。

WPFでは、画面が閉じられるタイミングで停止します。

C#
protected override void OnClosed(EventArgs e)
{
_timer.Stop();
base.OnClosed(e);
}

System.Timers.TimerSystem.Threading.TimerDisposeします。

C#
_timer?.Dispose();

イベントを明示的に解除する場合は、次のようにします。

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

長く動作するアプリでは、タイマーの停止忘れがメモリリークや意図しない処理継続の原因になることがあります。

9-6. 非同期処理中の例外を安全に扱う方法

タイマーイベント内で非同期処理を書く場合、例外処理を必ず入れます。

C#
private async void Timer_Tick(object? sender, EventArgs e)
{
try
{
await DoWorkAsync();
}
catch (OperationCanceledException)
{
// キャンセル時の処理
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}
}

Task.Delayのループでも同じです。

C#
while (!token.IsCancellationRequested)
{
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
Console.WriteLine($"エラー: {ex.Message}");
}

await Task.Delay(TimeSpan.FromSeconds(5), token);
}

例外を放置すると、タイマー処理が途中で止まったり、アプリ全体が終了したりする可能性があります。

10. C#タイマー実装のベストプラクティス

10-1. 用途に合ったタイマーを選ぶ

C#タイマーでは、最初に用途を明確にすることが大切です。

目的選ぶもの
WPFのUI更新DispatcherTimer
WinFormsのUI更新System.Windows.Forms.Timer
非同期の待機Task.Delay
非同期の定期処理Task.Delay + CancellationToken
バックグラウンドの軽量定期処理System.Threading.Timer
イベント形式の定期処理System.Timers.Timer

「何となくTimerを使う」のではなく、UI更新なのか、バックグラウンド処理なのか、非同期処理なのかを先に決めましょう。

10-2. 長時間処理はタイマー内で直接実行しない

タイマーのイベントやコールバック内に重い処理を直接書くと、次のような問題が起きます。

  • UIが固まる

  • 処理が重複する

  • タイマー間隔がずれる

  • 例外処理が複雑になる

重い処理は別メソッドに分け、必要に応じてTask.Runや非同期メソッドを使います。

C#
private async void Timer_Tick(object? sender, EventArgs e)
{
if (_isRunning) return;

_isRunning = true;

try
{
await Task.Run(() => HeavyWork());
}
finally
{
_isRunning = false;
}
}

10-3. キャンセル処理を必ず用意する

無限ループや定期処理には、停止手段を用意しましょう。Task.Delayを使う場合はCancellationTokenが基本です。

C#
private CancellationTokenSource? _cts;

public void Start()
{
_cts = new CancellationTokenSource();
_ = RunAsync(_cts.Token);
}

public void Stop()
{
_cts?.Cancel();
}

停止できないタイマー処理は、アプリ終了時や画面遷移時に問題になりやすいです。

10-4. 例外処理を明示的に書く

タイマー処理はバックグラウンドで実行されることが多いため、例外が見落とされやすいです。

C#
try
{
// タイマー処理
}
catch (Exception ex)
{
// ログ出力など
Console.WriteLine(ex);
}

特に定期実行では、1回の失敗で処理全体が止まらないようにすることが重要です。

10-5. DisposeやStopでリソースを解放する

タイマーを使い終わったら、必ず停止または破棄します。

C#
_timer.Stop();
_timer.Dispose();

DispatcherTimerStopSystem.Threading.TimerSystem.Timers.TimerDisposeまで行うと安全です。

画面やサービスのライフサイクルに合わせて、開始と停止の位置を明確にしておきましょう。

10-6. テストしやすいタイマー処理にする

タイマー処理をテストしやすくするには、タイマーそのものと実際の処理を分けることが大切です。

悪い例です。

C#
private void Timer_Tick(object? sender, EventArgs e)
{
// すべての処理をここに書く
}

良い例です。

C#
private void Timer_Tick(object? sender, EventArgs e)
{
ExecuteJob();
}

public void ExecuteJob()
{
// テストしたい処理
}

処理本体を通常のメソッドに分けておけば、タイマーを動かさなくても単体テストしやすくなります。

11. C#タイマーに関するよくある質問

11-1. C#で一番よく使うTimerはどれか

用途によりますが、UI更新ならWPFではDispatcherTimer、WinFormsではSystem.Windows.Forms.Timerがよく使われます。バックグラウンド処理ならSystem.Threading.TimerSystem.Timers.Timer、非同期処理ならTask.Delayがよく使われます。

現在のC#では、async/awaitと組み合わせやすいTask.Delayを使う場面も増えています。

11-2. TimerとTask.Delayはどちらを使うべきか

定期的にイベントやコールバックを発生させたいならTimer、非同期処理の中で一定時間待ちたいならTask.Delayが向いています。

たとえば、5秒待ってから次の処理に進むだけならTask.Delayが簡単です。

C#
await Task.Delay(5000);

一方、アプリの起動中ずっと1秒ごとに処理したい場合は、Timer系やTask.Delayループを検討します。

11-3. C#で1秒ごとに処理するにはどう書くか

コンソールアプリなら、System.Threading.Timerで次のように書けます。

C#
private static Timer? _timer;

static void Main()
{
_timer = new Timer(
_ => Console.WriteLine(DateTime.Now),
null,
TimeSpan.Zero,
TimeSpan.FromSeconds(1));

Console.ReadLine();
_timer.Dispose();
}

非同期で書くなら、Task.Delayを使います。

C#
while (true)
{
Console.WriteLine(DateTime.Now);
await Task.Delay(1000);
}

実務では、無限ループにせずCancellationTokenで停止できるようにします。

11-4. WPFで画面を定期更新するには何を使うべきか

WPFで画面を定期更新するなら、基本的にはDispatcherTimerを使います。UIスレッド上でTickイベントが実行されるため、画面部品を直接更新できます。

C#
private readonly DispatcherTimer _timer = new();

public MainWindow()
{
InitializeComponent();

_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += (_, _) =>
{
ClockText.Text = DateTime.Now.ToString("HH:mm:ss");
};
_timer.Start();
}

ただし、重い処理はTick内に直接書かず、バックグラウンドで実行しましょう。

11-5. タイマー処理を途中で止めるにはどうすればよいか

使っているタイマーによって停止方法が異なります。

種類停止方法
System.Threading.TimerChange(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan)またはDispose()
System.Timers.TimerStop()またはDispose()
DispatcherTimerStop()
Task.DelayループCancellationTokenでキャンセル

Task.Delayの場合は、次のようにCancellationTokenSourceを使います。

C#
using var cts = new CancellationTokenSource();

var task = RunAsync(cts.Token);

cts.Cancel();

await task;

停止後に再利用しない場合は、Disposeも忘れずに行いましょう。

まとめ

C#でタイマー処理を実装する方法は複数あります。重要なのは、目的に合ったタイマーを選ぶことです。

WPFで画面を更新するならDispatcherTimer、WinFormsならSystem.Windows.Forms.Timer、バックグラウンドで軽く定期処理したいならSystem.Threading.Timer、イベント形式で扱いたいならSystem.Timers.Timer、非同期処理の待機やキャンセル可能なループにはTask.Delayが向いています。

特に注意すべきポイントは、実行スレッド、UI更新、処理の重複、例外処理、停止・破棄です。タイマー処理は一見簡単に見えますが、アプリの種類や処理内容によって正しい書き方が変わります。

迷ったときは、次の基準で選ぶと分かりやすいです。

やりたいこと選ぶもの
WPFの表示を更新したいDispatcherTimer
一定時間待ちたいTask.Delay
非同期処理を繰り返したいTask.Delay + CancellationToken
バックグラウンドで定期実行したいSystem.Threading.Timer
イベント形式で定期実行したいSystem.Timers.Timer

C#タイマーを正しく使い分ければ、一定間隔処理、指定時間後の実行、UI更新、タイムアウト、カウントダウンなどを安全で読みやすく実装できます。