定期タスクとタイマー
PeriodicTimerとSystem.Threading.Timerで定期的な処理をスケジュールし、キャンセルを適切に管理します。
「定期タスクとタイマー」はCoddyKit上の無料C# Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはC# Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 C# Academyコースには全4レッスンが含まれています。
繰り返し処理のスケジュール設定
多くのバックグラウンドサービスでは、5分ごと、1時間ごと、毎日など、決まったスケジュールで処理を実行する必要があります。.NETには、PeriodicTimer(最新の方式)、System.Threading.Timer(従来の方式)、Quartz.NET(cronベース)など、複数の仕組みがあります。
PeriodicTimer:最新の選択肢
PeriodicTimer(.NET 6で導入)は、asyncメソッドで使用するように設計されています。コールバックを使わず、スケジュールどおりに正確に通知し、キャンセルとも自然に統合できます。
public class MetricsCollectorService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
while (await timer.WaitForNextTickAsync(ct))
{
// Runs every 60 seconds
await CollectMetricsAsync(ct);
}
// Loop exits cleanly when ct is cancelled
}
}PeriodicTimerとTask.Delayループの比較
PeriodicTimerはTask.Delayループよりも優れています。処理時間を加えた時点ではなく、開始時刻を基準に一定間隔で通知するため、多数の反復処理を行ってもずれが蓄積しません。
// Task.Delay loop — drifts! (interval = delay + work time)
while (!ct.IsCancellationRequested)
{
await DoWorkAsync(ct); // takes 2 seconds
await Task.Delay(5000, ct); // actual period: 7 seconds
}
// PeriodicTimer — no drift
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync(ct))
{
await DoWorkAsync(ct); // period is still 5 seconds from start
}System.Threading.Timer
System.Threading.Timerは、コールバックを使用する従来型のスレッドプールタイマーです。周期実行と1回限りの実行の両方をサポートします。ただし、コールバックはasyncをサポートしないスレッドプールスレッド上で実行されるため、注意が必要です。
public class LegacyTimerService : BackgroundService, IDisposable
{
private Timer? _timer;
protected override Task ExecuteAsync(CancellationToken ct)
{
// Fire immediately then every 10 seconds
_timer = new Timer(DoWork, null,
dueTime: TimeSpan.Zero,
period: TimeSpan.FromSeconds(10));
return Task.CompletedTask;
}
private void DoWork(object? state)
{
// Runs on thread pool — avoid async void here
Console.WriteLine($"Timer fired: {DateTime.UtcNow}");
}
public override void Dispose() { _timer?.Dispose(); base.Dispose(); }
}1回限りの遅延実行
1回限りの遅延実行には、PeriodicTimerを1回のティックで使用するか、CancellationTokenSource.CancelAfterとTask.Delayを組み合わせて使用します。
// Run once after a delay
public async Task ScheduleOnceAsync(TimeSpan delay, Func<Task> action, CancellationToken ct)
{
await Task.Delay(delay, ct);
await action();
}
// Or with CancellationToken:
var cts = new CancellationTokenSource(TimeSpan.FromHours(1));
try
{
await Task.Delay(Timeout.Infinite, cts.Token);
}
catch (OperationCanceledException)
{
await RunScheduledJobAsync();
}PeriodicTimerのエラー処理
1回の失敗によって次の周期のタイマー通知まで停止しないように、各ティックの処理は必ずtry/catchで囲んでください。
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(ct))
{
try
{
await SyncInventoryAsync(ct);
}
catch (Exception ex)
{
// Log but don't crash — next tick will retry
_logger.LogError(ex, "Inventory sync failed");
}
}
}時刻の境界への合わせ方
毎正時など特定の時刻に実行するには、タイマーを開始する前に、次の時刻の境界までの初期待機時間を計算します。
protected override async Task ExecuteAsync(CancellationToken ct)
{
// Wait until the next full hour
var now = DateTime.UtcNow;
var nextHour = now.Date.AddHours(now.Hour + 1);
var initialDelay = nextHour - now;
await Task.Delay(initialDelay, ct);
// Then tick every hour exactly
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
await RunJobAsync(ct); // run at the first tick
while (await timer.WaitForNextTickAsync(ct))
await RunJobAsync(ct);
}ティックの同時実行を防ぐ
処理にタイマーの周期より長い時間がかかる可能性がある場合は、SemaphoreSlimで同時実行を防ぐか、処理中であればそのティックをスキップしてください。
private readonly SemaphoreSlim _lock = new(1, 1);
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(ct))
{
// Skip if previous tick is still running
if (!await _lock.WaitAsync(0, ct))
{
_logger.LogWarning("Skipping tick — previous still running");
continue;
}
try { await DoWorkAsync(ct); }
finally { _lock.Release(); }
}
}分散サービスのためのジッター
サービスの複数のインスタンスが同時に起動する場合は、初期待機時間にランダムなジッターを加えることで、共有リソースへの集中アクセスを防げます。
protected override async Task ExecuteAsync(CancellationToken ct)
{
// Add jitter: 0-5 seconds random delay on startup
var jitter = TimeSpan.FromSeconds(Random.Shared.NextDouble() * 5);
await Task.Delay(jitter, ct);
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
while (await timer.WaitForNextTickAsync(ct))
await RunJobAsync(ct);
}実践例:キャッシュ更新サービス
ジッターとエラー回復を取り入れ、10分ごとに分散キャッシュを更新する周期実行サービスです。
public class CacheRefreshService : BackgroundService
{
private readonly IServiceScopeFactory _factory;
private readonly ILogger<CacheRefreshService> _log;
protected override async Task ExecuteAsync(CancellationToken ct)
{
// Spread startups across instances
await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(0, 30)), ct);
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(10));
while (await timer.WaitForNextTickAsync(ct))
{
try
{
using var scope = _factory.CreateScope();
var cache = scope.ServiceProvider.GetRequiredService<ICacheService>();
await cache.RefreshAllAsync(ct);
_log.LogInformation("Cache refreshed at {Time}", DateTime.UtcNow);
}
catch (Exception ex) { _log.LogError(ex, "Cache refresh failed"); }
}
}
}理解度チェック
繰り返し処理では、なぜTask.DelayループよりPeriodicTimerが推奨されるのですか?
まとめ:周期処理とタイマー
重要なポイント:
PeriodicTimer:async対応、キャンセル対応、ずれなし — 最新の選択肢ですSystem.Threading.Timer:コールバックベースで、レガシーコードとの統合に適しています- エラーでタイマーが停止しないよう、各ティックの処理をtry/catchで囲みます
- 分散されたインスタンスにはジッターを追加して、集中アクセスを回避します
- 前回の処理がまだ実行中の場合にティックをスキップするには、
SemaphoreSlimを使用します - 初期待機時間を計算し、ティックを時刻の境界に合わせます
よくある質問
「定期タスクとタイマー」レッスンは無料ですか?
はい。「定期タスクとタイマー」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、C# Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 C# Academyコースには全4レッスンが含まれています。
「定期タスクとタイマー」で何を学びますか?
PeriodicTimerとSystem.Threading.Timerで定期的な処理をスケジュールし、キャンセルを適切に管理します。 ブラウザで直接実行するハンズオンコードでC# Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
C# Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのC# Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「定期タスクとタイマー」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このC# Academyレッスンでコードを書いて実行できますか?
はい。すべてのC# Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。