はじめに 「スケジューラ」は“時間で動くバッチのミニ版”
「毎日 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/await と Task.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# プロジェクトに自然に組み込めるようになります。
