在 Asp.Net Core 中,咱們經常使用 System.Threading.Timer 這個定時器去作一些須要長期在後臺運行的任務,可是這個定時器在某些場合卻不太靈光,並且經常沒法控制啓動和中止,咱們須要一個穩定的,相似 WebHost 這樣主機級別的任務管理程序,可是又要比 WebHost 要輕便。git
由此,我找到了官方推薦的 IHostedService 接口,該接口位於程序集 Microsoft.Extensions.Hosting.Abstractions 的 命名空間 Microsoft.Extensions.Hosting。該接口自 .Net Core 2.0 開始提供,按照官方的說法,因爲該接口的出現,下面的這些應用場景的代碼均可以刪除了。github
歷史場景列表數據庫
- 輪詢數據庫以查找更改的後臺任務
- 從 Task.Run() 開始的後臺任務
- 按期更新某些緩存的計劃任務
- 容許任務在後臺線程上執行的 QueueBackgroundWorkItem 實現
- 在 Web 應用後臺處理消息隊列中的消息,同時共享 ILogger 等公共服務
1.1 首先來看接口 IHostedService 的代碼,這須要花一點時間去理解它的原理,你也能夠跳過本段直接進入第二段緩存
namespace Microsoft.Extensions.Hosting { // // Summary: // Defines methods for objects that are managed by the host. public interface IHostedService { // // Summary: // Triggered when the application host is ready to start the service. Task StartAsync(CancellationToken cancellationToken); // // Summary: // Triggered when the application host is performing a graceful shutdown. Task StopAsync(CancellationToken cancellationToken); } }
1.2 很是簡單,只有兩個方法,可是很是重要,這兩個方法分別用於程序啓動和退出的時候調用,這和 Timer 有着雲泥之別,這是質變。安全
1.3 從看到 IHostedService 這個接口開始,我就習慣性的想,按照微軟的慣例,某個接口必然有其默認實現的抽象類,而後我就看到了 Microsoft.Extensions.Hosting.BackgroundService ,果真,前人種樹後人乘涼,在 BackgroundService 類中,接口已經實現好了,咱們只須要去實現 ExecuteAsync 方法app
1.4 BackgroundService 內部代碼以下,值得注意的是 BackgroundService 從 .Net Core 2.1 開始提供,因此,使用舊版本的同窗們可能須要升級一下async
public abstract class BackgroundService : IHostedService, IDisposable { private Task _executingTask; private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource(); protected abstract Task ExecuteAsync(CancellationToken stoppingToken); public virtual Task StartAsync(CancellationToken cancellationToken) { // Store the task we're executing _executingTask = ExecuteAsync(_stoppingCts.Token); // If the task is completed then return it, // this will bubble cancellation and failure to the caller if (_executingTask.IsCompleted) { return _executingTask; } // Otherwise it's running return Task.CompletedTask; } public virtual async Task StopAsync(CancellationToken cancellationToken) { // Stop called without start if (_executingTask == null) { return; } try { // Signal cancellation to the executing method _stoppingCts.Cancel(); } finally { // Wait until the task completes or the stop token triggers await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken)); } } public virtual void Dispose() { _stoppingCts.Cancel(); } }
1.5 BackgroundService 內部實現了 IHostedService 和 IDisposable 接口,從代碼實現能夠看出,BackgroundService 充分實現了任務啓動註冊和退出清理的邏輯,並保證在任務進入 GC 的時候及時的退出,這很重要。ide
2.1 首先創一個通用的任務管理類 BackManagerService ,該類繼承自 BackgroundServiceui
public class BackManagerService : BackgroundService { BackManagerOptions options = new BackManagerOptions(); public BackManagerService(Action<BackManagerOptions> options) { options.Invoke(this.options); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // 延遲啓動 await Task.Delay(this.options.CheckTime, stoppingToken); options.OnHandler(0, $"正在啓動託管服務 [{this.options.Name}]...."); stoppingToken.Register(() => { options.OnHandler(1, $"託管服務 [{this.options.Name}] 已經中止"); }); int count = 0; while (!stoppingToken.IsCancellationRequested) { count++; options.OnHandler(1, $" [{this.options.Name}] 第 {count} 次執行任務...."); try { options?.Callback(); if (count == 3) throw new Exception("模擬業務報錯"); } catch (Exception ex) { options.OnHandler(2, $" [{this.options.Name}] 執行託管服務出錯", ex); } await Task.Delay(this.options.CheckTime, stoppingToken); } } public override Task StopAsync(CancellationToken cancellationToken) { options.OnHandler(3, $" [{this.options.Name}] 因爲進程退出,正在執行清理工做"); return base.StopAsync(cancellationToken); } }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { ... while (!stoppingToken.IsCancellationRequested) { ... await Task.Delay(this.options.CheckTime, stoppingToken); } }
while 循環內部使用 Task.Delay 設置時間,在 this.options.CheckTime 計時結束後繼續下一輪的調度任務
實際上,Task.Delay 方法內部也是使用了 System.Threading.Timer 類進行計時,可是,當內部的 Timer 計時結束後,會立刻被 Dispose 掉this
2.2 任務管理類 BackManagerService 包含一個帶參數的構造方法,是一個匿名委託,須要傳入參數 BackManagerOptions,該參數表示一個任務的調度參數
2.3 建立 BackManagerOptions 任務調度操做類
public class BackManagerOptions { /// <summary> /// 任務名稱 /// </summary> public string Name { get; set; } /// <summary> /// 獲取或者設置檢查時間間隔,單位:毫秒,默認 10 秒 /// </summary> public int CheckTime { get; set; } = 10 * 1000; /// <summary> /// 回調委託 /// </summary> public Action Callback { get; set; } /// <summary> /// 執行細節傳遞委託 /// </summary> public Action<BackHandler> Handler { get; set; } /// <summary> /// 傳遞內部信息到外部組件中,以方便處理擴展業務 /// </summary> /// <param name="level">0=Info,1=Debug,2=Error,3=exit</param> /// <param name="message"></param> /// <param name="ex"></param> /// <param name="state"></param> public void OnHandler(int level, string message, Exception ex = null, object state = null) { Handler?.Invoke(new BackHandler() { Level = level, Message = message, Exception = ex, State = state }); } }
2.4 該 BackManagerOptions 任務調度操做類包含了一些基礎的設置內容,好比任務名稱,執行週期間隔,回調委託 Callback,任務管理器內部執行細節傳遞委託 Handler,這些定義很是有用,下面會用到
2.5 其中,執行細節傳遞委託 Handler 包含一個參數,其實就是傳遞的細節,很是簡單的一個實體對象類,無非就是信息級別,消息描述,異常信息,執行對象
public class BackHandler { /// <summary> /// 0=Info,1=Debug,2=Error /// </summary> public int Level { get; set; } public string Message { get; set; } public Exception Exception { get; set; } public object State { get; set; } }
2.6 定義好上面的 3 個對象後,如今來建立一個訂單管理類,用於定時輪詢數據庫訂單是否超時未付款,而後返還庫存
public class OrderManagerService { public void CheckOrder() { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("==業務執行完成=="); Console.ForegroundColor = ConsoleColor.Gray; } public void OnBackHandler(BackHandler handler) { switch (handler.Level) { default: case 0: break; case 1: case 3: Console.ForegroundColor = ConsoleColor.Yellow; break; case 2: Console.ForegroundColor = ConsoleColor.Red; break; } Console.WriteLine("{0} | {1} | {2} | {3}", handler.Level, handler.Message, handler.Exception, handler.State); Console.ForegroundColor = ConsoleColor.Gray; if (handler.Level == 2) { // 服務執行出錯,進行補償等工做 } else if (handler.Level == 3) { // 退出事件,清理你的業務 CleanUp(); } } public void CleanUp() { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("==清理完成=="); Console.ForegroundColor = ConsoleColor.Gray; } }
2.7 這個 OrderManagerService 業務類定義了 3 個方法,CheckOrder 檢查訂單,OnBackHandler 輸出執行信息,CleanUp 在程序退出的時候去作一些清理工做,很是簡單,前兩個方法是用於註冊到 BackManagerService 任務調度器中,後一個是內部方法。
3.1 定義好業務類後,咱們須要把它註冊到進程中,以便程序啓動和退出的時候自動執行
3.2 在 Startup.cs 的 ConfigureServices 方法中註冊託管主機,看下面的代碼
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); services.AddSingleton<Microsoft.Extensions.Hosting.IHostedService, BackManagerService>(factory => { OrderManagerService order = new OrderManagerService(); return new BackManagerService(options => { options.Name = "訂單超時檢查"; options.CheckTime = 5 * 1000; options.Callback = order.CheckOrder; options.Handler = order.OnBackHandler; }); }); }
3.3 上面的代碼經過將 BackManagerService 註冊到託管主機中,並在初始化的時候設置了 BackManagerOptions ,而後將 OrderManagerService 的方法註冊到 BackManagerOptions 的委託中,實現業務執行
3.4 運行程序,觀察輸出結果
3.4 輸出結果清晰的表示建立的託管服務運行良好,咱們來看一下執行順序
執行順序
- 啓動託管服務
- 執行「訂單超時檢查」任務,連續執行了 3 次,間隔 5 秒,每次執行都向外部傳遞了執行細節信息
- 因爲咱們故意設置任務執行到第 3 次的時候模擬拋出異常,能夠看到,異常被正確的捕獲並安全的傳遞到外部
- 任務繼續執行
- 強制終止了程序,而後託管服務收到了程序中止的信號並當即進行了清理工做,通知外部業務委託執行清理
- 清理完成,託管服務中止並退出
3.5 註冊多個託管服務,經過定義的 BackManagerService 任務調度器,咱們甚至具有了同時託管數個任務的能力,而咱們只須要在 ConfigureServices 增長一行代碼
public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); services.AddSingleton<Microsoft.Extensions.Hosting.IHostedService, BackManagerService>(factory => { OrderManagerService order = new OrderManagerService(); return new BackManagerService(options => { options.Name = "訂單超時檢查"; options.CheckTime = 5 * 1000; options.Callback = order.CheckOrder; options.Handler = order.OnBackHandler; }); }); services.AddSingleton<Microsoft.Extensions.Hosting.IHostedService, BackManagerService>(factory => { OrderManagerService order = new OrderManagerService(); return new BackManagerService(options => { options.Name = "成交數量統計"; options.CheckTime = 2 * 1000; options.Callback = order.CheckOrder; options.Handler = order.OnBackHandler; }); }); }
3.6 爲了方便,咱們仍是使用 OrderManagerService 來模擬業務,只是把任務名稱改爲 "成交數量統計",並設置任務執行週期間隔爲 2 秒
3.7 如今來運行程序,觀察輸出
3.8 輸出結果正常,兩個託管服務獨立運行,互不干擾,藍色爲 "成交數量統計",白色爲 "訂單超時檢查"
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // 延遲啓動 await Task.Delay(this.options.CheckTime, stoppingToken); ... }
public static void Main(string[] args) { CreateWebHostBuilder(args) .UseShutdownTimeout(TimeSpan.FromSeconds(15)) .Build().Run(); }
https://github.com/lianggx/EasyAspNetCoreDemo/tree/master/Ron.BackHost