动态

详情 返回 返回

C#.NET Cronos 實戰:優雅解析與執行 Cron 表達式 - 动态 详情

簡介

Cronos 是一個專為 .NET 設計的輕量級、高性能 cron 表達式解析庫,由 Hangfire 團隊開發。相比其他 cron 庫,它專注於提供精確的時區處理和高效的計算算法,特別適合需要跨時區調度的現代應用。

  • .NET 應用中需要使用 Cron 表達式驅動定時任務時,系統自帶的定時器並不支持直接解析 Cron 語法。
  • 雖然有 Quartz.NET 等重量級調度框架,但在只需表達式解析與下一次執行時間計算的場景中,引入整套框架過於臃腫。
  • Cronos 是一款專注於 Cron 表達式解析與下次執行時間計算的輕量級庫,支持標準與擴展語法、時區、秒級精度,並且性能優越、易於集成。

解決的關鍵痛點

  • 精確時區轉換:原生支持從任意時區計算觸發時間
  • 無狀態解析:線程安全的表達式解析
  • 高效算法:O(1) 時間複雜度的下次觸發計算
  • 輕量級:無外部依賴,適合 Serverless 環境

安裝與配置

  • NuGet 安裝
Install-Package Cronos
  • 命名空間引用
using Cronos;

Cronos 支持 .NET Standard 2.0+,兼容所有現代 .NET 平台(.NET Core/.NET 5+/Mono 等)。

核心功能

  • 多種表達式格式

    • 標準 5 段(分鐘、小時、日、月、周)
    • 擴展 6 段(秒、分鐘、小時、日、月、周)
    • 可選年字段(7 段:秒、分、時、日、月、周、年)
  • 下次與多次執行時間計算

    • GetNextOccurrence(DateTime baseTime, TimeZoneInfo tz = null)
    • GetOccurrences(DateTime start, DateTime end, TimeZoneInfo tz = null)
  • 時區支持

    • 可傳入 TimeZoneInfo,自動根據時區與夏令時規則計算本地時間
  • 異常反饋

    • 對非法表達式拋出 CronExpressionException,內含錯誤位置提示
  • 線程安全

    • CronExpression 實例可在多線程間共享
  • 性能

    • 解析與計算採用高效算法,適合高併發環境

主要 API 用法

// 1. 解析表達式
CronExpression expr = CronExpression.Parse("*/15 8-18 * * MON-FRI", CronFormat.Standard);

// 2. 獲取下一次執行時間(本地時區)
DateTime? next = expr.GetNextOccurrence(DateTime.Now);

// 3. 獲取下一次執行時間(指定時區)
var tz = TimeZoneInfo.FindSystemTimeZoneById("China Standard Time");
DateTime? nextInCst = expr.GetNextOccurrence(DateTime.UtcNow, tz);

// 4. 枚舉一段時間內所有執行時間
DateTime start = DateTime.Now;
DateTime end = start.AddDays(1);
IEnumerable<DateTime> occs = expr.GetOccurrences(start, end);

// 5. 捕獲非法表達式異常
try {
    CronExpression.Parse("invalid cron");
} catch (CronFormatException ex) {
    Console.WriteLine($"表達式語法錯誤:{ex.Message}");
}
  • CronExpression.Parse(string, CronFormat):顯式指定格式(Standard 或 Seconds)。
  • GetNextOccurrence(DateTime, TimeZoneInfo = null):如果區間外返回 null。
  • GetOccurrences(DateTime start, DateTime end, TimeZoneInfo = null):懶枚舉,按需生成。

使用示例

示例 1:每天下午 6 點執行

var expr = CronExpression.Parse("0 18 * * *"); // 標準 5 段
var next = expr.GetNextOccurrence(DateTime.Now);
Console.WriteLine($"下一次:{next}");

示例 2:工作日上午 9:30、下午 15:30 執行

var expr = CronExpression.Parse("30 9,15 * * MON-FRI");
foreach (var dt in expr.GetOccurrences(DateTime.Today, DateTime.Today.AddDays(1))) {
    Console.WriteLine(dt);
}

示例 3:支持秒級,間隔 10 秒執行

var expr = CronExpression.Parse("*/10 * * * * *", CronFormat.IncludeSeconds);
await foreach (var dt in expr.GetOccurrencesAsync(DateTime.UtcNow, DateTime.UtcNow.AddMinutes(1))) {
    Console.WriteLine(dt);
}

結合 PeriodicTimer

結合 PeriodicTimer 實現簡單調度器:

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

class Program
{
    static async Task Main()
    {
        var expression = CronExpression.Parse("0 */5 * * * *", CronFormat.IncludeSeconds); // 每 5 秒
        var timeZone = TimeZoneInfo.Utc;
        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1)); // 每秒檢查
        using var cts = new CancellationTokenSource();

        try
        {
            var next = expression.GetNextOccurrence(DateTimeOffset.UtcNow, timeZone);
            while (await timer.WaitForNextTickAsync(cts.Token))
            {
                var now = DateTimeOffset.UtcNow;
                if (next.HasValue && now >= next.Value)
                {
                    Console.WriteLine($"Task executed at {now:HH:mm:ss}");
                    next = expression.GetNextOccurrence(now, timeZone);
                }
            }
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("Scheduler stopped");
        }
    }
}

結合 Quartz.NET

結合 Quartz.NET 實現完整調度:

using Cronos;
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: {DateTimeOffset.Now:HH:mm:ss}");
        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 cronExpression = CronExpression.Parse("0 0 8 * * ?");
        var trigger = TriggerBuilder.Create()
            .WithIdentity("myTrigger", "group1")
            .WithCronSchedule(cronExpression.ToString())
            .Build();

        await scheduler.ScheduleJob(job, trigger);
        Console.ReadLine(); // 保持運行
    }
}

性能優化策略

表達式預編譯

// 靜態存儲預編譯表達式
private static readonly CronExpression _precompiledCron = 
    CronExpression.Parse("0 */15 * * * *", CronFormat.IncludeSeconds);

// 使用時直接調用
DateTime? next = _precompiledCron.GetNextOccurrence(DateTime.UtcNow);

批處理優化

// 批量計算未來100次執行
public List<DateTime> GetNextOccurrencesBatch(
    CronExpression cron, 
    int count, 
    TimeZoneInfo tz)
{
    var results = new List<DateTime>(count);
    DateTime? current = DateTime.UtcNow;
    
    for (int i = 0; i < count; i++)
    {
        current = cron.GetNextOccurrence(current.Value, tz);
        if (!current.HasValue) break;
        results.Add(current.Value);
    }
    
    return results;
}

常見使用場景

輕量級定時任務

  • Console、Worker Service 中驅動週期性作業,無需完整 Quartz

動態調度配置

  • 從數據庫或配置文件加載 Cron 表達式,運行時動態調整執行計劃。

延遲/重試機制

  • 配合消息隊列,實現複雜的重試策略(如間隔指數級、以下午/深夜時段執行等)。

報表/統計

  • 定時生成日報、月報,可通過表達式控制「最後一個工作日」等複雜規則。

與其他工具的對比

  • NCrontab

    • 廣泛用於 Azure Functions 和其他 .NET 應用。
    • 支持五字段和六字段表達式,但夏令時處理不如 Cronos 直觀。
    • 性能略低於 Cronos(解析約 100 ns vs 30 ns)。
    • 社區支持更強,文檔豐富。
  • Cronos

    • 更現代的 API,支持 DateTimeOffset 和時區。
    • 夏令時處理更符合 Vixie Cron
    • 性能更高,適合高頻任務。

資源和文檔

  • NuGet 包:https://www.nuget.org/packages/Cronos
  • GitHub 倉庫:https://github.com/HangfireIO/Cronos
  • Cron 表達式測試工具:https://crontab.guru

總結

Cronos 是現代 .NET 應用中處理 cron 表達式的首選輕量級解決方案,尤其適合:

  • 需要精確跨時區調度的全球性應用
  • 運行在資源受限環境(如 AWS Lambda
  • 高頻觸發的定時任務(>1次/秒)
  • Hangfire 深度集成的調度系統
user avatar tpwonline 头像 manongsir 头像
点赞 2 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.