C#タイマーの使い方完全ガイド|Timer・DispatcherTimer・Task.Delayの違いと実装例
はじめに
C#で「一定時間ごとに処理したい」「5秒後に1回だけ実行したい」「画面の時計表示を1秒ごとに更新したい」と考えたとき、多くの場合に使うのがタイマー処理です。
ただし、C#には名前の似たタイマーが複数あります。代表的なものだけでも、System.Threading.Timer、System.Timers.Timer、WPFのDispatcherTimer、WinFormsのTimer、そしてタイマーの代わりによく使われるTask.Delayがあります。
どれも「時間を扱う」点では似ていますが、実行されるスレッド、UI更新との相性、非同期処理との組み合わせやすさ、停止や破棄の方法が異なります。選び方を間違えると、UI更新時に例外が出たり、処理が重複したり、タイマーが停止しなかったりする原因になります。
この記事では、C#タイマーの基本から、Timer・DispatcherTimer・Task.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.DelayやSystem.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 | イベントベースの定期処理 |
DispatcherTimer | WPFのUI更新 |
Task.Delay | 非同期処理での待機・繰り返し処理 |
WinFormsのSystem.Windows.Forms.Timerについても比較として触れますが、中心はTimer・DispatcherTimer・Task.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が固まります。
| 種類 | 主な対象 |
|---|---|
DispatcherTimer | WPF |
System.Windows.Forms.Timer | WinForms |
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 | 最初に実行するまでの待ち時間 |
period | 2回目以降の実行間隔 |
上の例では、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回だけ実行したい場合は、periodにTimeout.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"));
}
}
停止するときは、dueTimeとperiodの両方にTimeout.InfiniteTimeSpanを指定します。
3-5. Disposeでタイマーを正しく破棄する方法
タイマーが不要になったらDisposeで破棄します。
C#_timer?.Dispose();
_timer = null;
System.Threading.TimerはIDisposableを実装しているため、使い終わったら明示的に破棄することが大切です。破棄しないと、不要なコールバックが残り続ける原因になります。
クラス内で管理する場合は、次のように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は、StartとStopで制御できます。
C#_timer.Start(); // 開始
_timer.Stop(); // 停止
ボタンやコマンドのような操作に合わせて開始・停止したい場合、System.Threading.TimerのChangeより直感的に扱えます。
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.TimerとSystem.Timers.Timerの大きな違いは、書き方と制御方法です。
| 比較項目 | System.Threading.Timer | System.Timers.Timer |
|---|---|---|
| 書き方 | コールバック | Elapsedイベント |
| 開始・停止 | Change | Start / Stop |
| 1回実行 | Timeout.Infinite | AutoReset = 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で実行してはいけない理由
DispatcherTimerのTickイベント内で時間のかかる処理を直接実行すると、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.DelayはCancellationTokenを受け取れます。これにより、待機中の処理を途中でキャンセルできます。
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.DelayとThread.Sleepは似ていますが、動作は大きく異なります。
| 比較項目 | Task.Delay | Thread.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.Timer | ThreadPool |
System.Timers.Timer | 主にThreadPool |
DispatcherTimer | WPFのUIスレッド |
System.Windows.Forms.Timer | WinFormsのUIスレッド |
Task.Delay | 待機後の継続処理はコンテキスト次第 |
UI更新が必要なら、UIスレッドで動くDispatcherTimerやWinFormsのTimerが扱いやすいです。バックグラウンド処理なら、System.Threading.TimerやSystem.Timers.Timerが向いています。
7-2. UI更新に向いているかの違い
UI更新に向いているかどうかを比較すると、次のようになります。
| 種類 | UI更新 |
|---|---|
DispatcherTimer | WPFで向いている |
System.Windows.Forms.Timer | WinFormsで向いている |
System.Threading.Timer | 直接更新は不可 |
System.Timers.Timer | 直接更新は注意 |
Task.Delay | UIイベント内のawaitなら扱いやすい |
WPFでSystem.Threading.TimerからUIを更新したい場合は、Dispatcher.InvokeやDispatcher.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.TimerやDispatcherTimerでも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を使う場合は、StartとStopを呼び分けます。
C#_timer.Start();
_timer.Stop();
8-4. 一定時間ごとにAPIやファイルをチェックする
非同期APIを定期的に呼び出すなら、Task.DelayとCancellationTokenの組み合わせが扱いやすいです。
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.TimerやSystem.Threading.TimerはDisposeします。
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();
DispatcherTimerはStop、System.Threading.TimerやSystem.Timers.TimerはDisposeまで行うと安全です。
画面やサービスのライフサイクルに合わせて、開始と停止の位置を明確にしておきましょう。
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.TimerやSystem.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.Timer | Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan)またはDispose() |
System.Timers.Timer | Stop()またはDispose() |
DispatcherTimer | Stop() |
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更新、タイムアウト、カウントダウンなどを安全で読みやすく実装できます。

