.Net Core 如何实现简易定时任务?

我在《如何实现文章浏览量高并发计数?》一文中,有提及关于如何使用定时器的问题,本文对此问题展开讨论。.NET Core 实现定时任务有很多种方式,比如使用第三方框架:Quartz.net 或者 Hangfire,还有就是 .NET 自身的 Timer,但文本要讲述的是使用另一种方式实现后台任务。这个方式就是基于.NET Core 中 IHostedService实现定时任务。

从 .Net Core 2.0 开始,IHostedService 被正式引入,可以通过其来实现后台任务,但只能基于 WebHost。从 .Net Core 2.1 开始微软引入通用主机(Generic Host),在不使用 Web 的情况下,也可以使用 IHostedService 来实现基于控制台、Windows 服务的后台任务,并且引入了 BackgroundService 抽象类,更为方便快捷的创建后台任务。

IHostedService 后台任务的执行与(主机或微服务)应用程序的生命周期相协调。 当应用程序启动的同时开启定时任务,当应用程序关闭时,在停止定时任务前,还可以执行一些清理操作。在没有IHostedService时,当然也能启动后台线程来运行任何任务,但是当应用关闭时,会直接终止线程,并不能执行正常的清理操作。

当注册IHostedService时,.NET Core 会在应用程序启动和停止期间分别调用IHostedService的 StartAsync() 和 StopAsync() 方法。 具体而言,即在服务器启动并触发 IApplicationLifetime 的 ApplicationStarted 后调用 StartAsync。

namespace Microsoft.Extensions.Hosting 
{ 
   // 
   // 摘要: 
   //     Defines methods for objects that are managed by the host. 
   public interface IHostedService 
   { 
       // 
       // 摘要: 
       //     Triggered when the application host is ready to start the service. 
       // 
       // 参数: 
       //   cancellationToken: 
       //     Indicates that the start process has been aborted. 
       Task StartAsync(CancellationToken cancellationToken); 
       // 
       // 摘要: 
       //     Triggered when the application host is performing a graceful shutdown. 
       // 
       // 参数: 
       //   cancellationToken: 
       //     Indicates that the shutdown process should no longer be graceful. 
       Task StopAsync(CancellationToken cancellationToken); 
   } 
}

我们可以创建并实现 IHostedService自定义服务类,但由于大多数后台任务,在取消令牌管理和其他操作方面都有类似的需求,因此 .Net Core 2.1 有一个非常方便的可派生的抽象基类,BackgroundService定义如下:

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(); 
   } 
}

基于上面的抽象基类,我们可以定义并实现一个简单的后台定时任务服务抽象基类:

public abstract class ScheduedService : IHostedService, IDisposable 
{ 
   private readonly Timer _timer; 
   private readonly TimeSpan _period; 
   protected readonly ILogger _logger; 
   protected bool IsExecuting { get; private set; } 
   protected ScheduedService(TimeSpan period, ILogger logger) 
   { 
       _logger = logger; 
       _period = period; 
       _timer = new Timer(Execute, null, Timeout.Infinite, 0); 
   } 
   private void Execute(object state = null) 
   { 
       try 
       { 
           IsExecuting = true; 
           _logger.LogDebug("启动执行服务"); 
           ExecuteAsync().Wait(); 
       } 
       catch (Exception ex) 
       { 
           _logger.LogError(ex, "执行异常"); 
       } 
       finally 
       { 
           IsExecuting = false; 
           _logger.LogDebug("执行完成"); 
       } 
   } 
   protected abstract Task ExecuteAsync(); 
   public virtual void Dispose() 
   { 
       _timer?.Dispose(); 
   } 
   public virtual Task StartAsync(CancellationToken cancellationToken) 
   { 
       _logger.LogDebug("服务正在启动"); 
       var random = new Random(); 
       _timer.Change(TimeSpan.FromSeconds(random.Next(10)), _period); 
       return Task.CompletedTask; 
   } 
   public virtual Task StopAsync(CancellationToken cancellationToken) 
   { 
       _logger.LogDebug("正在停止服务"); 
       _timer?.Change(Timeout.Infinite, 0); 
       return Task.CompletedTask; 
   } 
}

基于我们自定义的抽象基类ScheduedService,使用Timer实现后台文章浏览更新服务定时任务类:

public class PostViewUpdateService : ScheduedService 
{ 
   private readonly IPostViewService _postViewService; 
   private readonly IServiceProvider _serviceProvider; 
   public PostViewUpdateService(ILogger<PostViewUpdateService> logger, 
       IPostViewService postViewService, 
       IServiceProvider serviceProvider) 
       : base(TimeSpan.FromMinutes(15), logger) 
   { 
       _serviceProvider = serviceProvider; 
       _postViewService = postViewService; 
   } 
   protected override async Task ExecuteAsync() 
   { 
       await UpdateViewsAsync(); 
   } 
   public override Task StartAsync(CancellationToken cancellationToken) 
   { 
       _postViewService.Start(); 
       return base.StartAsync(cancellationToken); 
   } 
   public override async Task StopAsync(CancellationToken cancellationToken) 
   { 
       if (!IsExecuting) 
       { 
           await UpdateViewsAsync(); 
       } 
       _postViewService.Stop(); 
       await base.StopAsync(cancellationToken); 
   } 
   private async Task UpdateViewsAsync() 
   { 
       using (var scope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) 
       { 
           var postRepo = scope.ServiceProvider.GetService<IPostRepository>(); 
           //1、增量更新文章浏览数 
           var postIds = _postViewService.IncreViews.Keys; 
           foreach (var postId in postIds) 
           { 
               if (_postViewService.IncreViews.TryRemove(postId, out int views)) 
               { 
                   await postRepo.UpdateViewsAsync(postId, views); 
               } 
               else 
               { 
                   _logger.LogWarning($"读取增量文章(Id:{postId})数据失败"); 
               } 
           } 
       } 
   } 
}

这个类实现的是每 15 分钟更新文章浏览量到数据库,文章浏览更新服务IPostViewService可以参考文章《如何实现文章浏览量高并发计数?》。

在程序启动类的配置服务中注册上述定时任务类,这样,我们在 .Net Core 中实现了简易的定时任务,有兴趣的读者可以通过本站的文章浏览量计数观察效果。

《.Net Core 如何实现简易定时任务?》的相关评论

发表评论

必填项已用 * 标记,邮箱地址不会被公开。