C# Tips | 日付・時間処理:スケジューラ

C# C#
スポンサーリンク

はじめに 「スケジューラ」は“時間で動くバッチのミニ版”

「毎日 3:00 にバックアップしたい」
「5分ごとにキューをポーリングしたい」
「指定時刻になったら一度だけ処理したい」

こういう“時間をトリガーにして処理を動かす仕組み”が、ここでいう「スケジューラ」です。
本格的なジョブスケジューラ(Windows タスクスケジューラや Quartz.NET)もありますが、
業務アプリの中で「ちょっとした定期実行」をしたいだけなら、
自前の小さなスケジューラユーティリティで十分な場面も多いです。

ここでは、初心者向けに

  • 一回だけ指定時刻に実行する
  • 一定間隔で繰り返し実行する
  • 毎日決まった時刻に実行する

という“ミニスケジューラ”を、C# でどう書くかをかみ砕いて説明します。


基本形:指定時刻に一回だけ実行するスケジューラ

「〇時〇分になったら一度だけ実行したい」

まずは、一番シンプルな「指定した日時に一回だけ処理を実行する」スケジューラです。
DateTime で「実行したい時刻」を受け取り、
「今からその時刻までの待ち時間」を計算して Task.Delay で待つ、という形にします。

using System;
using System.Threading;
using System.Threading.Tasks;

public static class SimpleScheduler
{
    public static async Task ScheduleOnceAsync(
        DateTime runAt,
        Func<Task> action,
        CancellationToken cancellationToken = default)
    {
        if (action == null) throw new ArgumentNullException(nameof(action));

        TimeSpan delay = runAt - DateTime.Now;

        if (delay <= TimeSpan.Zero)
        {
            await action();
            return;
        }

        await Task.Delay(delay, cancellationToken);

        if (!cancellationToken.IsCancellationRequested)
        {
            await action();
        }
    }
}
C#

使い方の例です。

DateTime runAt = DateTime.Now.AddSeconds(10);

await SimpleScheduler.ScheduleOnceAsync(runAt, async () =>
{
    Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 一回だけ実行されました。");
});
C#

ここでの重要ポイントは、
「実行時刻そのものではなく、“今からどれくらい待つか”に変換している」ことです。
runAt - DateTime.Now で待ち時間を TimeSpan にし、それを Task.Delay に渡す、という流れを覚えておくと、
「指定時刻に実行する」系の処理が一気に書きやすくなります。


一定間隔で繰り返し実行するスケジューラ

「5分ごとに処理したい」をユーティリティ化する

次に、「一定間隔で同じ処理を繰り返す」スケジューラです。
タイマーでも書けますが、ここでは async/awaitTask.Delay で素直に書いてみます。

public static class IntervalScheduler
{
    public static async Task RunIntervalAsync(
        TimeSpan interval,
        Func<Task> action,
        CancellationToken cancellationToken = default)
    {
        if (action == null) throw new ArgumentNullException(nameof(action));
        if (interval <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(interval));

        while (!cancellationToken.IsCancellationRequested)
        {
            await action();

            try
            {
                await Task.Delay(interval, cancellationToken);
            }
            catch (TaskCanceledException)
            {
                break;
            }
        }
    }
}
C#

使い方の例です。

using var cts = new CancellationTokenSource();

var task = IntervalScheduler.RunIntervalAsync(
    TimeSpan.FromSeconds(5),
    async () =>
    {
        Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] 5秒ごとの処理");
    },
    cts.Token);

Console.WriteLine("Enter を押すと停止します。");
Console.ReadLine();
cts.Cancel();

await task;
C#

ここでの重要ポイントは、「ループの中で action を実行し、そのあと Task.Delay で待つ」という構造です。
この“実行→待つ→実行→待つ…”のパターンが、シンプルなスケジューラの基本形になります。
また、CancellationToken を受け取っておくことで、「いつ止めるか」を外から制御できるようにしています。


毎日決まった時刻に実行するスケジューラ

「毎日 3:00 にだけ実行したい」

業務でよくあるのが、「毎日 3:00 にバッチを実行したい」というパターンです。
これは「次に実行すべき日時」を毎回計算し、その時刻まで待つ、という形で書けます。

public static class DailyScheduler
{
    public static async Task RunDailyAsync(
        TimeSpan timeOfDay,
        Func<Task> action,
        CancellationToken cancellationToken = default)
    {
        if (action == null) throw new ArgumentNullException(nameof(action));

        while (!cancellationToken.IsCancellationRequested)
        {
            DateTime now = DateTime.Now;
            DateTime todayRun = now.Date + timeOfDay;

            DateTime nextRun = todayRun > now
                ? todayRun
                : todayRun.AddDays(1);

            TimeSpan delay = nextRun - now;

            try
            {
                await Task.Delay(delay, cancellationToken);
            }
            catch (TaskCanceledException)
            {
                break;
            }

            if (!cancellationToken.IsCancellationRequested)
            {
                await action();
            }
        }
    }
}
C#

使い方の例です。

using var cts = new CancellationTokenSource();

var task = DailyScheduler.RunDailyAsync(
    new TimeSpan(3, 0, 0), // 毎日 3:00
    async () =>
    {
        Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 日次バッチ実行");
    },
    cts.Token);

// 実際のサービスなら、アプリのライフタイムに合わせて cts.Cancel() を呼ぶ
C#

ここでの重要ポイントは、「毎回ループの中で“次に実行すべき日時”を計算している」ことです。
now.Date + timeOfDay で「今日の指定時刻」を作り、
それが過去なら翌日にずらす、というロジックになっています。
このパターンを覚えておくと、「毎週月曜 9:00」などにも応用できます。


スケジューラ設計で意識してほしいポイント

例外処理と止まり方

スケジューラの中で実行する処理が例外を投げたとき、
そのままタスクが落ちてしまうと「以降二度と動かない」状態になりがちです。
実務では、action の中身を try-catch で囲み、ログを出してから続行する、
あるいはリトライ回数を決める、といった設計が必要になります。

例えば、簡易的にはこう書けます。

try
{
    await action();
}
catch (Exception ex)
{
    Console.WriteLine($"スケジューラ内で例外: {ex}");
}
C#

「止めるときは CancellationToken で止める」「例外で勝手に止まらないようにする」
この2つを意識しておくと、スケジューラが“勝手に死んでいた”事故をかなり防げます。

時刻の基準とタイムゾーン

日次スケジューラなどでは、「どのタイムゾーンの何時か」が重要です。
日本時間で 3:00 なのか、UTC で 3:00 なのか、
サーバーのローカルタイムに依存してよいのか、を最初に決めておく必要があります。

シンプルな社内システムなら「サーバーは日本時間固定」「DateTime.Now ベース」で割り切ることも多いですが、
将来的にクラウドや複数リージョンを意識するなら、
DateTimeOffset や UTC ベースの設計も検討したほうがよいです。


まとめ 「スケジューラユーティリティ」は“時間で動く処理の型”

スケジューラは、
「時間が来たら動く」「一定間隔で動く」という処理の“型”をコードにするものです。

押さえておきたいポイントを整理すると、こうなります。

指定時刻実行は「実行時刻 − 今」で待ち時間を出し、Task.Delay で待ってから処理する。
一定間隔実行は「実行→待つ」をループで回し、CancellationToken で止められるようにする。
日次実行は「次に実行すべき日時」を毎回計算し、その時刻まで待つ。
スケジューラ内の処理は例外でタスクごと死なないように、try-catch とログを意識する。
どのタイムゾーン・どの時刻基準で動かすかを、最初に決めてコード全体で統一する。

ここまで理解できれば、
「とりあえず while(true) で回してる」状態から一歩抜け出して、
“業務・実務でちゃんと使えるスケジューラユーティリティ”を、自分の C# プロジェクトに自然に組み込めるようになります。

C#C#
スポンサーリンク
シェアする
@lifehackerをフォローする
スポンサーリンク
タイトルとURLをコピーしました