在上個月寫過一篇 .NET 純原生實作 Cron 定時任務執行,未依賴第三方組件 的文章,當時 CronSchedule 的實作是使用了,每個服務都獨立進入到一個 while 回圈中,進行定期掃描是否到了執行時間來實作的,但是那個邏輯有些問題,經過各位朋友的測驗,發現當多個任務的時候存在一定概率不按照計劃執行的情況,

感謝各位朋友的積極探討,多交流一起進步,之前那個 while 回圈的邏輯每回圈一次 Task.Delay 1000 毫秒,無限回圈,多個任務的時候還會同時有多個回圈任務,確實不夠好,
所以決定重構 CronSchedule 的實作,采用全域使用一個 Timer 的形式,每隔 1秒鐘掃描一次任務佇列看看是否有需要執行的任務,整體的實作思路還是之前的,如果沒有看過之前那篇文章的建議先看一下,本片主要針對調整部分進行說明 .NET 純原生實作 Cron 定時任務執行,未依賴第三方組件 ,主要調整了 CronSchedule.cs
using Common; using System.Reflection; namespace TaskService.Libraries { public class CronSchedule { private static List<ScheduleInfo> scheduleList = new(); private static Timer mainTimer; public static void Builder(object context) { var taskList = context.GetType().GetMethods().Where(t => t.GetCustomAttributes(typeof(CronScheduleAttribute), false).Length > 0).ToList(); foreach (var action in taskList) { string cron = action.CustomAttributes.Where(t => t.AttributeType == typeof(CronScheduleAttribute)).FirstOrDefault()!.NamedArguments.Where(t => t.MemberName == "Cron" && t.TypedValue.Value != null).Select(t => t.TypedValue.Value!.ToString()).FirstOrDefault()!; scheduleList.Add(new ScheduleInfo { CronExpression = cron, Action = action, Context = context }); } if (mainTimer == default) { mainTimer = new(Run, null, 0, 1000); } } private static void Run(object? state) { var nowTime = DateTime.Parse(DateTimeOffset.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")); foreach (var item in scheduleList) { if (item.LastTime != null) { var nextTime = DateTime.Parse(CronHelper.GetNextOccurrence(item.CronExpression, item.LastTime.Value).ToString("yyyy-MM-dd HH:mm:ss")); if (nextTime == nowTime) { item.LastTime = DateTimeOffset.Now; _ = Task.Run(() => { item.Action.Invoke(item.Context, null); }); } } else { item.LastTime = DateTimeOffset.Now.AddSeconds(5); } } } private class ScheduleInfo { public string CronExpression { get; set; } public MethodInfo Action { get; set; } public object Context { get; set; } public DateTimeOffset? LastTime { get; set; } } } [AttributeUsage(AttributeTargets.Method)] public class CronScheduleAttribute : Attribute { public string Cron { get; set; } } }
這里的邏輯改為了注入任務時將 mainTimer 實體化啟動,每一秒鐘執行1次 Run方法,Run 方法內部用于 回圈檢測 scheduleList 中的任務,如果時間符合,則啟動一個 Task 去執行對應的 Action,這樣全域不管注冊多少個服務,也只有一個 Timer 在回圈運行,相對之前的 CronSchedule 實作相對更好一點,
使用的時候方法基本沒怎么改,只是調整了CronSchedule.Builder 的呼叫 代碼如下:
using DistributedLock; using Repository.Database; using TaskService.Libraries; namespace TaskService.Tasks { public class DemoTask : BackgroundService { private readonly IServiceProvider serviceProvider; private readonly ILogger logger; public DemoTask(IServiceProvider serviceProvider, ILogger<DemoTask> logger) { this.serviceProvider = serviceProvider; this.logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { CronSchedule.Builder(this); await Task.Delay(-1, stoppingToken); } [CronSchedule(Cron = "0/1 * * * * ?")] public void ClearLog() { try { using var scope = serviceProvider.CreateScope(); var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>(); //省略業務代碼 Console.WriteLine("ClearLog:" + DateTime.Now); } catch (Exception ex) { logger.LogError(ex, "DemoTask.ClearLog"); } } [CronSchedule(Cron = "0/5 * * * * ?")] public void ClearCache() { try { using var scope = serviceProvider.CreateScope(); var db = scope.ServiceProvider.GetRequiredService<DatabaseContext>(); var distLock = scope.ServiceProvider.GetRequiredService<IDistributedLock>(); //省略業務代碼 Console.WriteLine("ClearCache:" + DateTime.Now); } catch (Exception ex) { logger.LogError(ex, "DemoTask.ClearCache"); } } } }
然后啟動我們的專案就可以看到如下的運行效果:

最上面連著兩個 16:25:53 并不是重復呼叫了,只是因為這個任務配置的是 1秒鐘執行1次,第一次啟動任務的時候執行的較為耗時,導致第一次執行和第二次執行進入到方法中的時間差太短了,這個只在第一次產生,對后續的執行計劃沒有影響,
至此 .NET 純原生實作 Cron 定時任務執行,未依賴第三方組件 (Timer 優化版) 就講解完了,有任何不明白的,可以在文章下面評論或者私信我,歡迎大家積極的討論交流,有興趣的朋友可以關注我目前在維護的一個 .NET 基礎框架專案,專案地址如下 https://github.com/berkerdong/NetEngine.git https://gitee.com/berkerdong/NetEngine.git轉載請註明出處,本文鏈接:https://www.uj5u.com/net/505204.html
標籤:.NET Core
