在 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