一般在 .NET Core 中,我们将这些类型的任务称为托管服务 ,因为它们是托管在主机/应用程序/微服务中的服务/逻辑。 请注意,在这种情况下,托管服务仅表示具有后台任务逻辑的类。
前言
从 .net core 2.0 开始,开始引入 IHostedService
,可以通过 IHostedService
来实现后台任务,但是只能在 WebHost
的基础上使用。从 .net core 2.1 开始微软引入通用主机(Generic Host
),使得我们可以在不使用 Web 的情况下,也可以使用 IHostedService
来实现 定时任务/Windows服务/后台任务,并且引入了一个 BackgroundService
抽象类来更方便的创建后台任务。 基本理念是,可以注册多个后台任务(托管服务),在 Web 主机或主机运行时在后台运行,如下图所示:
ASP.NET Core 2.0 中的 WebHost(实现 IWebHost 的基类)是用于为进程提供 HTTP 服务器功能的基础结构项目,例如,如果正在实现 MVC Web 应用或 Web API 服务。 它提供 ASP.NET Core 中所有新的基础结构优点,使用户能够使用依赖关系注入,在请求管道中插入中间件等,并精确地将这些 IHostedServices 用于后台任务。
.NET Core 2.1 中引入了 Host(实现 IHost 的基类)。 基本上,Host 能让用户拥有与 WebHost(依赖项注入、托管服务等)相似的基础结构,但在这种情况下,只需拥有一个简单轻便的进程作为主机,与 MVC、Web API 或 HTTP 服务器功能无关。
因此,可以选择一个专用主机进程,也可使用 IHost 创建一个来专门处理托管服务,例如仅用于托管 IHostedServices 的微服务,或者可以选择性地扩展现有的 ASP.NET Core WebHost,例如现有的 ASP.NET Core Web API 或 MVC 应用。
每种方法都有优缺点,具体取决于业务和可伸缩性需求。 重要的是,如果后台任务与 HTTP (IWebHost) 无关,则应使用 IHost。
IHostedService 接口
IHostedService
后台任务的执行与应用程序(就此而言,为主机或微服务)的生存期相协调。 当应用程序启动时注册任务,当应用程序关闭时,有机会执行某些正常操作或清理。
始终可以启动后台线程来运行任何任务,而无需使用 IHostedService
。 不同之处就在于应用的关闭时间,此时会直接终止线程,而没有机会执行正常的清理操作。
当注册 IHostedService
时,.NET Core 会在应用程序启动和停止期间分别调用 IHostedService
类型的 StartAsync()
和 StopAsync()
方法。 具体而言,即在服务器已启动并已触发 IApplicationLifetime.ApplicationStarted
后调用 start。
在 .NET Core 中定义的 IHostedService
如下所示。
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 的多个实现,并在 ConfigureService()
方法中将它们注册到 DI 容器中,如前所示。 所有这些托管服务将随应用程序/微服务一起启动和停止。
当主机触发 StopAsync()
方法时,需负责处理服务的停止操作。
使用从 BackgroundService 基类派生的自定义托管服务类来实现 IHostedService
可以从头开始创建自定义托管服务类并实现 IHostedService
,因为在使用 .NET Core 2.0 时需执行这些操作。
但是,由于大多数后台任务在取消令牌管理和其他典型操作方面都有类似的需求,因此有一个非常方便且可以从中进行派生的抽象基类,名为 BackgroundService
(自 .NET Core 2.1 起提供)。
该类提供设置后台任务所需的主要工作。
下一个代码是在 .NET Core 中实现的抽象 BackgroundService 基类。
// Copyright (c) .NET Foundation. Licensed under the Apache License, Version 2.0.
/// <summary>
/// Base class for implementing a long running <see cref="IHostedService"/>.
/// </summary>
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();
}
}
从上一抽象基类派生时,得益于该继承的实现,用户只需在自定义的托管服务类中实现 ExecuteAsync()
方法 ,使用以上作为基类实现简单定时任务:
class TimedBackgroundService : BackgroundService
{
private Timer _timer;
private int i = 0;
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5)); //每5秒开启一个线程
return Task.CompletedTask;
}
private void DoWork(object state)
{
i++;
int name = i; //i表示这是第几个线程
for (int j = 0; j < 100; j++)
{
Console.WriteLine($"这里是线程{name}: 正在执行j = {j}");
Task.Delay(TimeSpan.FromSeconds(1)).Wait(); //等待1秒
}
}
public override Task StartAsync(CancellationToken cancellationToken)
{
return ExecuteAsync(cancellationToken);
}
public override void Dispose()
{
base.Dispose();
_timer?.Dispose();
}
}
向 WebHost
或 Host
添加一个或多个 IHostedServices
的方式是,通过 ASP.NET Core WebHost
(或 .NET Core 2.1 及更高版本中的 Host
)中的 AddHostedService 扩展方法对它们进行注册。 基本上,必须在常见的 Startup
类的 ConfigureServices()
方法中注册托管服务,如以下典型的 ASP.NET WebHost 中的代码所示。
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddHostedService<TimedBackgroundService>();
}
上图显示多个与 IHostedService 相关的类和接口的类图 。
类图: IWebHost 和 IHost 可以托管许多服务,这些服务从实现 IHostedService 的 BackgroundService 继承。
IHostedService实现一个的后台定时任务
根据所了解的信息,可以基于 IHostedService
实现一个简单的后台定时任务服务
public abstract class ScheduedService : IHostedService, IDisposable
{
protected readonly Timer _timer;
private readonly TimeSpan _period;
protected readonly ILogger logger;
protected bool IsDisallowConcurrentExecution = false;
protected ScheduedService(TimeSpan period)
{
logger = LogManager.GetCurrentClassLogger();
_period = period;
_timer = new Timer(Execute, null, Timeout.Infinite, 0);
}
public void Execute(object state = null)
{
if (IsDisallowConcurrentExecution)
{
_timer?.Change(Timeout.Infinite, 0);
}
try
{
ExecuteAsync().Wait();
}
catch (Exception ex)
{
logger.Error(ex, $"Execute exception:{ex}");
}
if (IsDisallowConcurrentExecution)
{
_timer?.Change(_period _period);
}
}
protected abstract Task ExecuteAsync();
public virtual void Dispose()
{
_timer?.Dispose();
}
public Task StartAsync(CancellationToken cancellationToken)
{
logger.Info("Schedued Service is starting.");
_timer?.Change(TimeSpan.FromSeconds(RandomHelper.Next(10)), _period);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
logger.Info("Schedued Service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
}
根据上面抽象类使用Timer实现的后台定时任务类实现一个定时任务:
public class ProcessExamsDataService : ScheduedService
{
public ProcessExamsDataService() : base(TimeSpan.FromSeconds(4))
{
IsDisallowConcurrentExecution = true;
}
protected override Task ExecuteAsync()
{
string guid = Guid.NewGuid().ToString("N");
Console.WriteLine($"定时任务{guid}执行开始:{DateTime.Now.ToLongDateString()} {DateTime.Now.ToLongTimeString()}");
System.Threading.Thread.Sleep(1000*20);
Console.WriteLine($"定时任务{guid}执行结束:{DateTime.Now.ToLongDateString()} {DateTime.Now.ToLongTimeString()}");
return Task.FromResult(true);
}
}
在程序启动的时候注册服务:
services.AddHostedService<ProcessExamsDataService>();
部署注意事项和要点
请务必注意,部署 ASP.NET Core WebHost
或 .NET Core Host
的方式可能会影响最终解决方案。 例如,如果在 IIS 或常规 Azure 应用服务上部署 WebHost
,由于应用池回收,主机可能会被关闭。 但是,如果将主机作为容器部署到 Kubernetes 或 Service Fabric 等业务流程协调程序中,则可以控制主机的实时实例数量。 此外,还可以考虑云中专门针对这些方案的其他方法,例如 Azure Functions。 最后,如果需要服务一直处于运行状态并在 Windows Server 上部署,可以使用 Windows 服务。
但即使对于部署到应用池中的 WebHost
,也存在如重新填充或刷新应用程序的内存中缓存这样的情况,这仍然适用。
IHostedService
接口为在 ASP.NET Core Web 应用程序(在 .NET Core 2.0 中)或任何进程/主机(从使用 IHost
的 .NET Core 2.1 开始)中启动后台任务提供了一种便捷方式。 其主要优势在于,当主机本身将要关闭时,可以有机会进行正常取消以清理后台任务的代码。