0Pricing
C# Academy · レッスン

定期タスクとタイマー

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フィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. IHostedServiceとBackgroundService
  2. Worker Serviceプロジェクト
  3. 定期タスクとタイマー
  4. Quartz.NETのスケジュールジョブ
← C# Academyに戻る