简介
NCrontab 是 .NET 平台下功能完备的 Cron 表达式解析与调度计算库,用于处理类似 Unix Cron 的时间调度逻辑。它不依赖外部系统服务,纯托管实现,是构建定时任务系统的核心组件。
解决的关键问题
-
Cron表达式解析:将字符串表达式转换为可计算的时间模型 -
时间序列生成:计算下次执行时间或生成时间序列
-
跨平台支持:纯
.NET实现,无操作系统依赖 -
轻量高效:无外部依赖,内存占用低(<100KB)
相比于自己手写解析器或引入重量级调度框架(如 Quartz.NET),NCrontab 专注于表达式分析和下一次运行时间计算,体积轻巧、依赖少、性能高。
Cron表达式格式详解
- 标准格式(5段式)
text
* * * * *
┬ ┬ ┬ ┬ ┬
│ │ │ │ │
│ │ │ │ └── 星期几 (0-6, 0=周日)
│ │ │ └─────── 月份 (1-12)
│ │ └──────────── 日 (1-31)
│ └───────────────── 小时 (0-23)
└────────────────────── 分钟 (0-59)
- 扩展格式(6段式,支持秒级)
text
* * * * * *
┬ ┬ ┬ ┬ ┬ ┬
│ │ │ │ │ │
│ │ │ │ │ └── 星期几 (0-6)
│ │ │ │ └─────── 月份 (1-12)
│ │ │ └──────────── 日 (1-31)
│ │ └───────────────── 小时 (0-23)
│ └────────────────────── 分钟 (0-59)
└─────────────────────────── 秒 (0-59)
- 特殊字符说明
| 字符 | 含义 | 示例 | 说明 |
|---|---|---|---|
* |
任意值 | * * * * * |
每分钟执行 |
, |
值列表 | 0,15,30 * * * * |
每小时的0,15,30分执行 |
- |
范围 | 9-17 * * * * |
9点到17点每小时执行 |
/ |
步长 | */5 * * * * |
每5分钟执行 |
? |
不指定(仅用于日和星期) | 0 0 ? * 1 |
每周一午夜 |
L |
最后 (Last) | 0 0 L * * |
每月最后一天午夜执行 |
W |
最近工作日(Weekday) | 0 0 15W * * |
每月15日最近的工作日执行 |
# |
第N个星期X | 0 0 * * 1#2 |
每月第二个周一执行 |
安装与配置
shell
Install-Package NCrontab
NCrontab 兼容 .NET Framework 4.6.1+、.NET Standard 2.0+,以及所有 .NET Core/.NET 5+ 版本。
只需在代码文件顶部添加引用:
csharp
using NCrontab;
核心功能
Cron表达式解析
支持标准 5 段(分、时、日、月、周)格式,以及可选的第 6 段"年"字段扩展。
-
下次执行时间计算
-
CrontabSchedule.GetNextOccurrence(DateTime baseTime):获取从baseTime开始的下一条匹配时间。 -
CrontabSchedule.GetNextOccurrences(DateTime start, DateTime end):枚举指定时间范围内的所有匹配时间。
-
-
可配置解析选项
-
CrontabSchedule.Parse(string expression, CrontabSchedule.ParseOptions options):控制是否支持年字段或秒级字段。 -
CrontabSchedule.ParseOptions.IncludeSeconds(仅在扩展包NCrontab.Scheduler中支持)。
-
-
线程安全
CrontabSchedule实例在多线程间可安全共享,建议对同一表达式只调用一次Parse并缓存结果。
API 用法
| 方法 / 属性 | 说明 |
|---|---|
CrontabSchedule.Parse(string expression) |
解析 5 段标准 Cron 表达式,返回调度对象 |
CrontabSchedule.Parse(string expression, ParseOptions opt) |
按指定选项解析 Cron 表达式 |
DateTime GetNextOccurrence(DateTime baseTime) |
获取从 baseTime 之后的第一条匹配时间 |
IEnumerable<DateTime> GetNextOccurrences(DateTime start, DateTime end) |
获取指定时间区间内的所有匹配时间 |
string ToString() |
返回原始表达式文本 |
ParseOptions.IncludeSeconds |
true 时支持解析第 0 段(秒)字段;默认只支持分级别。 |
使用示例
- 基本示例:每小时第 15 分钟执行
csharp
// 解析表达式 "15 * * * *":每小时的第 15 分钟
var schedule = CrontabSchedule.Parse("15 * * * *");
// 获取下一次执行时间(相对于当前时间)
var next = schedule.GetNextOccurrence(DateTime.Now);
Console.WriteLine($"下一次执行时间:{next}");
// 枚举未来 24 小时内的所有执行时间
var now = DateTime.Now;
var list = schedule.GetNextOccurrences(now, now.AddHours(24));
foreach (var dt in list)
{
Console.WriteLine(dt);
}
- 支持年字段:每年 1 月 1 日凌晨 0 点
csharp
// 6 段表达式:"0 0 1 1 * *"(秒 分 时 日 月 周 年)
var opts = new CrontabSchedule.ParseOptions { IncludingSeconds = false, // NCrontab 默认不支持秒
// NCrontab 默认不支持年字段,需要扩展包或自定义支持
};
var yearly = CrontabSchedule.Parse("0 0 1 1 *", new CrontabSchedule.ParseOptions());
// 获取未来 5 次执行
var occs = yearly.GetNextOccurrences(DateTime.Now, DateTime.Now.AddYears(10)).Take(5);
foreach (var dt in occs) Console.WriteLine(dt);
高级功能详解
时区处理
csharp
// 创建带时区的调度器
var cron = CrontabSchedule.Parse("0 12 * * *", new CrontabSchedule.ParseOptions
{
IncludingSeconds = false // 使用5段式
});
// 转换到特定时区
var tz = TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time");
DateTime utcNow = DateTime.UtcNow;
// 计算东京时区的下次中午12点
DateTime next = cron.GetNextOccurrence(utcNow);
DateTime nextInTokyo = TimeZoneInfo.ConvertTimeFromUtc(next, tz);
复杂表达式解析
csharp
// 每月最后一个工作日上午10:15
var cron = CrontabSchedule.Parse("15 10 LW * *");
// 每月第三个周五下午3点
var cron = CrontabSchedule.Parse("0 15 * * 5#3");
// 工作日上午9点到下午6点,每10分钟
var cron = CrontabSchedule.Parse("*/10 9-18 * * Mon-Fri");
构建简单调度器
csharp
public class CronScheduler
{
private readonly CrontabSchedule _schedule;
private DateTime _nextRun;
public CronScheduler(string cronExpression)
{
_schedule = CrontabSchedule.Parse(cronExpression);
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
public async Task StartAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var now = DateTime.Now;
if (now >= _nextRun)
{
await ExecuteJobAsync();
_nextRun = _schedule.GetNextOccurrence(now);
}
await Task.Delay(TimeSpan.FromSeconds(30), ct); // 每30秒检查
}
}
private Task ExecuteJobAsync()
{
// 任务执行逻辑
Console.WriteLine($"任务于 {DateTime.Now} 执行");
return Task.CompletedTask;
}
}
在 ASP.NET Core 中使用
csharp
// Program.cs
builder.Services.AddHostedService<CronBackgroundService>();
// 后台服务实现
public class CronBackgroundService : BackgroundService
{
private readonly CrontabSchedule _cron;
private DateTime _nextRun;
public CronBackgroundService()
{
_cron = CrontabSchedule.Parse("0 */2 * * *"); // 每2小时
_nextRun = _cron.GetNextOccurrence(DateTime.Now);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var now = DateTime.Now;
if (now > _nextRun)
{
await DoHourlyTaskAsync();
_nextRun = _cron.GetNextOccurrence(now);
}
await Task.Delay(5000, stoppingToken); // 每5秒检查
}
}
}
错误处理策略
csharp
try
{
var schedule = CrontabSchedule.Parse(userInput);
}
catch (CrontabException ex)
{
// 捕获特定解析错误
logger.LogError($"无效的cron表达式: {userInput}, 错误: {ex.Message}");
// 提供默认表达式
schedule = CrontabSchedule.Parse("0 0 * * *");
}
性能优化技巧
csharp
// 缓存高频使用的调度器
private static readonly ConcurrentDictionary<string, CrontabSchedule> _scheduleCache = new();
public CrontabSchedule GetCachedSchedule(string cron)
{
return _scheduleCache.GetOrAdd(cron, CrontabSchedule.Parse);
}
// 批量计算优化
DateTime[] GetNextOccurrencesBatch(CrontabSchedule schedule, int count)
{
var results = new DateTime[count];
DateTime current = DateTime.Now;
for (int i = 0; i < count; i++)
{
current = schedule.GetNextOccurrence(current);
results[i] = current;
}
return results;
}
结合 Quartz.NET
NCrontab 可与 Quartz.NET 集成,用于更复杂的调度:
csharp
using Quartz;
using Quartz.Impl;
using System;
using System.Threading.Tasks;
public class MyJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
Console.WriteLine($"Job executed at: {DateTime.Now}");
return Task.CompletedTask;
}
}
class Program
{
static async Task Main()
{
var factory = new StdSchedulerFactory();
var scheduler = await factory.GetScheduler();
await scheduler.Start();
var job = JobBuilder.Create<MyJob>()
.WithIdentity("myJob", "group1")
.Build();
var trigger = TriggerBuilder.Create()
.WithIdentity("myTrigger", "group1")
.WithCronSchedule("0 0 8 * * ?") // 每天 8:00
.Build();
await scheduler.ScheduleJob(job, trigger);
}
}
使用 NCrontab.Scheduler
NCrontab.Scheduler 是基于 NCrontab 的轻量级调度器,支持动态添加任务:
csharp
using NCrontab.Scheduler;
class Program
{
static void Main()
{
var scheduler = new Scheduler();
scheduler.AddTask(CrontabSchedule.Parse("*/1 * * * *"), ct =>
{
Console.WriteLine($"Task runs every minute: {DateTime.Now:O}");
});
scheduler.Start();
Console.ReadLine(); // 保持运行
}
}
简单定时任务示例
csharp
public class CronJob
{
private readonly CrontabSchedule _schedule;
private DateTime _nextRun;
public CronJob(string cronExpression)
{
_schedule = CrontabSchedule.Parse(cronExpression);
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
public void CheckAndRun(Action action)
{
DateTime now = DateTime.Now;
if (now >= _nextRun)
{
action.Invoke();
_nextRun = _schedule.GetNextOccurrence(now);
}
}
}
// 使用示例:每小时执行一次
var hourlyJob = new CronJob("0 * * * *");
while (true)
{
hourlyJob.CheckAndRun(() => {
Console.WriteLine($"执行于: {DateTime.Now}");
});
Thread.Sleep(60_000); // 每分钟检查一次
}
封装为可配置服务
csharp
public class CronService : BackgroundService
{
private readonly List<CronJob> _jobs = new();
public void AddJob(string cron, Action action)
{
_jobs.Add(new CronJob(cron, action));
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
foreach (var job in _jobs)
{
job.CheckAndRun();
}
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
// 注册服务
services.AddHostedService<CronService>();
常见使用场景
适用场景
- 后台服务定时任务
在 ASP.NET Core、Windows Service 或 Worker Service 中,用来调度邮件发送、报表生成、缓存清理等周期性任务。
- 动态配置调度
从数据库或配置中心读取 Cron 表达式,并动态生成 CrontabSchedule 实例,允许业务人员无需重启即可调整调度策略。
- 微服务消息投递
结合消息队列(RabbitMQ、Kafka)实现延迟队列或定时重试功能。
不适用场景
-
高精度定时(<1秒级精度)
-
分布式协调任务(需用分布式调度器)
-
动态实时调整(表达式变更需重启)
-
长周期任务(超过5年的调度计算)
何时选择其他方案:
-
需要分布式任务调度 →
Quartz.NET -
需要任务持久化和重试 →
Hangfire -
需要复杂工作流管理 → Elsa Workflows
性能与注意事项
-
性能
-
解析开销:
Parse方法对表达式做词法和语法分析,建议对同一表达式只执行一次,并缓存CrontabSchedule实例。 -
计算开销:
GetNextOccurrence算法为线性扫描,遇到复杂范围(如"每月的最后一个工作日")时性能略有下降,但对常见表达式足够快速。
-
-
线程安全
CrontabSchedule的GetNext*方法可在多线程并发调用,无需额外同步。
-
时区问题
-
输入的
DateTime:NCrontab不涉及时区转换,所有计算均在DateTime自身的Kind上执行。 -
UTC vs Local:如果系统跨时区或夏令时环境,建议统一使用DateTime.UtcNow并将调度时间也转换为UTC。
-
-
表达式合法性
- 对于不合法的表达式,
Parse会抛出CrontabException。
- 对于不合法的表达式,
-
扩展限制
- 正式包不支持秒级(第 0 段)或年级(第 6 段)字段;社区扩展或自定义修改后可按需添加。
资源和文档
-
NuGet包:www.nuget.org/packages/NC... -
GitHub仓库:github.com/atifaziz/NC... -
NCrontab表达式测试工具:ncrontab.swimburger.net