后台任务
内置有后台任务基类,可快速实现后台任务,支持定时任务。
实现后台任务
BackgroundTask 为内置的后台任务抽象基类,通过从该类继承来实现具体的后台任务服务。 其中 DoWorkAsync 是任务具体执行方法,在继承类必须重写,其它属性/方法则按需重写。
// 实现一个后台任务服务:
[UseDI(typeof(MyBackgroundService))]
public class MyBackgroundService : BackgroundTask
{
// DoWorkAsync() 将被无限次循环执行
protected override async Task<IServiceResult> DoWorkAsync(CancellationToken stoppingToken)
{
Console.WriteLine("MyBackgroundService.DoWorkAsync()");
await Task.Delay(TimeSpan.FromSeconds(1));
return ServiceResult.Ok();
}
}
// 实现一个定时后台任务服务:
[UseDI(typeof(MyScheduledService))]
public class MyScheduledService : BackgroundTask
{
// 周期为 5 秒
public override int TimerTaskPeriodInMilliseconds => 5 * 1000;
// 5 秒执行一次 DoWorkAsync()
protected override async Task<IServiceResult> DoWorkAsync(CancellationToken stoppingToken)
{
WorkMessage = $"我的定时任务第 {WorkExecutingRound} 次执行";
Console.WriteLine(WorkMessage);
await Task.Delay(TimeSpan.FromSeconds(1));
return ServiceResult.Ok();
}
}
- TimerTaskPeriodInMilliseconds 大于零表示该任务是一个定时任务
- 如果任务要随应用一同启动但又想延迟一会再执行,可重写 DelayMillisecondsForFirstExecution 属性
- 如果你要任务执行失败时立即结束,可重写 BreakTaskWhenWorkFailed 并返回 true
- 用 PauseAsync/StopAsync 暂停/停止任务时, 并不会中止当前正在执行的 DoWorkAsync(),可通过检查自己的 Status 还是不是 Running 来获知
- 可通过 IsAppStopping 获知应用程序是否正在停止,此时可能某些资源已被释放了
注册并随应用启动
后台任务服务要以 Singleton 单例模式注入到系统中,在 Program.cs 里加入:
// 注册服务:
// (如果利用了 QuickAdmin.Net 提供的批量注册方法,可不用这样一个个注册,参见开发指南章节)
builder.Services.AddSingleton(typeof(MyBackgroundService));
builder.Services.AddSingleton(typeof(MyScheduledService));
// 让后台任务服务随应用一起启动起来:
// 以下只添加了 MyBackgroundService,因此 MyBackgroundService 会随应用立即启动起来,而 MyScheduledService 则为停止状态,
// 可去内置的后台任务管理页面,或自行在某处通过代码手工启动 MyScheduledService
builder.Services.AddHostedService<MyBackgroundService>(provider => provider.GetService<MyBackgroundService>());
注意在定义服务时加上 UseDI 特性并指定服务类型为自己,就像前边示例那样, 以确保当使用 QuickAdmin.Net 提供的批量注册方法时能够正确注入到系统中。
后台任务管理页面
系统内置的后台任务管理页面为:~/QAdmin/Setup/BkTasks。Demo 里后台任务管理菜单项位于 系统管理->系统配置 菜单下。
可在该页面里查看已加载任务信息,并进行启动/暂停/继续/停止等操作。
点击行首的加号或双击行将展开显示更多任务信息:
自动启动并保持运行
你可能需要确保后台任务能够自动运行起来(比如服务器重启后)。默认情况下 WebApp 在收到请求前是不会自动运行起来的,其中的后台任务自然也就不会运行。
另外你可能也需要后台任务能够一直保持运行状态。
要达到此目的,请参考以下设置。
后台任务代码
- 确保 DoWorkAsync() 里捕获并处理了所有异常:
[UseDI(typeof(MyBackgroundService))]
public class MyBackgroundService : BackgroundTask
{
protected override async Task<IServiceResult> DoWorkAsync(CancellationToken stoppingToken)
{
try
{
...
}
catch
{
}
}
}
- 让后台任务服务随应用一起启动起来,在 Program.cs 里加入:
builder.Services.AddHostedService<MyBackgroundService>(provider => provider.GetService<MyBackgroundService>());
宿主配置(IIS)
若是将 ASP.NET Core WebApp 发布到了 IIS,默认情况下 WebApp 并不会随 WWW 服务的启动而自动运行起来,并且由于应用程序池的默认回收机制, 导致 WebApp 会被周期性地关闭,其中的后台任务自然无法一直保持运行。
因此配置的目标便是让 WebApp 能随 WWW 服务的启动而自动运行起来,并且防止 WebApp 被周期性地关闭。具体操作参考以下步骤。
- 确保 WWW 服务的启动类型是 "自动"。默认是 "自动",除非你自行修改过。
- 安装 "应用程序初始化(Application Initialization)" 模块。安装该模块的目的是让 WebApp 在收到外部请求之前也能运行起来。
在 Windows Server 里(Server 2012+),打开服务器管理器,通过 添加角色和功能 → Web 服务器 (IIS) → Web 服务器 → 应用程序开发 → 应用程序初始化 安装:
在 Win10/Win11 里,通过 控制面板 → 程序 → 启用或关闭 Windows 功能 → Internet Information Services → 万维网服务 → 应用程序开发功能 → 应用程序初始化 安装:
- 配置应用程序池一直运行且禁止自动回收。
打开你的 WebApp 使用的应用程序池的高级配置,按下图所示配置:
- "启动模式" 设为 "AlwaysRunning",指示应用程序池一直运行
- "固定时间间隔" 默认为 1740 分钟,即应用程序池始终在启动后大约 29 小时强制重启,无论当前是否有用户在访问系统,将其改为 0 表示禁用定期回收
- "闲置超时" 默认为 20 分钟,即若 20 分钟内一直无请求,也会被回收,设为 0 则禁用空闲回收
- 配置 WebApp 启用预加载功能。
打开你的 WebApp 的高级配置,将 "预加载已启用" 置为 "True":
- 修改你的 WebApp 下的 web.config,确保使用进程内托管(发布应用时,已默认为 inprocess 了,除非你自行修改过),并配置 applicationInitialization:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<location path="." inheritInChildApplications="false">
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
</handlers>
<!-- 确保 hostingModel 为 "inprocess":-->
<aspNetCore processPath="dotnet" arguments=".\QAdminSampleApp.Web.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess" />
<!-- Application Initialization 模块配置: -->
<applicationInitialization
doAppInitAfterRestart="true"
skipManagedModules="false"
remapManagedRequestsTo="/" >
<add initializationPage="/" />
</applicationInitialization>
</system.webServer>
</location>
</configuration>
- doAppInitAfterRestart 设为 "true",使应用重启时自动启动初始化过程
- skipManagedModules 设为 "false",因为需要加载托管模块
- remapManagedRequestsTo 指示在初始化期间要将请求重新映射到的页,比如一个用来给用户提示应用正在启动中的静态页面
- initializationPage 指示初始化时自动请求的应用的 Url
使用进程内托管("inprocess"),WebApp 将直接在 w3wp.exe 进程中运行,更易与 IIS 的预加载机制集成。
加入 applicationInitialization 配置,将使 WebApp 即使没有收到外部请求也能自动运行起来。有关 Application Initialization 模块的更多信息请参阅官方文档。
验证效果
可在后台任务启动事件里去添加一条日志:
[UseDI(typeof(MyScheduledService))]
public class MyScheduledService : BackgroundTask
{
// 周期为 5 秒
public override int TimerTaskPeriodInMilliseconds => 5 * 1000;
// 5 秒执行一次 DoWorkAsync()
protected override async Task<IServiceResult> DoWorkAsync(CancellationToken stoppingToken)
{
WorkMessage = $"我的定时任务第 {WorkExecutingRound} 次执行";
Console.WriteLine(WorkMessage);
await Task.Delay(TimeSpan.FromSeconds(1));
return ServiceResult.Ok();
}
protected override async Task<IServiceResult> OnTaskStartingAsync(CancellationToken stoppingToken)
{
await AddExceptionLogAsync("我正在启动...");
return await base.OnTaskStartingAsync(stoppingToken);
}
}
编译、发布应用并将其部署到 IIS 下,按照本节所述进行配置,然后重启一下 WWW 服务并记住启动的时间,此时不要对应用发起任何请求(不要浏览任何页面或请求任何 API)。
然后过一会再用超级管理员登录系统,看看后台任务的启动时间是否基本就是 WWW 服务启动时间,在系统异常日志里也会有一条该时间点的记录。
