- 全球化与本土化
- 性能
- 高级
在 ASP.NET Core 中使用托管服务实现后台任务
作者:Luke Latham 和 Jeow Li Huan
在 ASP.NET Core 中,后台任务作为托管服务实现 。 托管服务是一个类,具有实现 IHostedService 接口的后台任务逻辑。 本主题提供了三个托管服务示例:
- 在计时器上运行的后台任务。
- 激活有作用域的服务的托管服务。 有作用域的服务可使用依赖项注入 (DI)。
- 按顺序运行的已排队后台任务。
辅助角色服务模板
ASP.NET Core 辅助角色服务模板可作为编写长期服务应用的起点。 通过辅助角色服务模板创建的应用将在其项目文件中指定 Worker SDK:
<Project Sdk="Microsoft.NET.Sdk.Worker">
要使用该模板作为编写托管服务应用的基础:
-
Visual Studio
- 创建新项目。
- 选择“辅助角色服务” 。 选择“下一步”。
- 在“项目名称”字段提供项目名称,或接受默认项目名称 。 选择“创建” 。
- 在“创建辅助角色服务”对话框中,选择“创建” 。
-
Visual Studio for Mac
- 创建新项目。
- 在侧栏中的“.NET Core” 下,选择“应用” 。
- 在“ASP.NET Core” 下,选择“辅助角色” 。 选择“下一步”。
- 对于“目标框架”,选择“.NET Core 3.0”或更高版本 。 选择“下一步”。
- 在“项目名称” 字段中提供名称。 选择“创建” 。
-
.NET Core CLI
将辅助角色服务 (
worker
) 模板用于命令行界面中的 dotnet new 命令。 下面的示例中创建了名为ContosoWorker
的辅助角色服务应用。 执行命令时会自动为ContosoWorker
应用创建文件夹。dotnet new worker -o ContosoWorker
Package
基于辅助角色服务模板的应用使用 Microsoft.NET.Sdk.Worker
SDK,并且具有对 Microsoft.Extensions.Hosting 包的显式包引用。 有关示例,请参阅示例应用的项目文件 (BackgroundTasksSample.csproj)。
对于使用 Microsoft.NET.Sdk.Web
SDK 的 Web 应用,通过共享框架隐式引用 Microsoft.Extensions.Hosting 包。 在应用的项目文件中不需要显式包引用。
IHostedService 接口
IHostedService 接口为主机托管的对象定义了两种方法:
StartAsync(CancellationToken) –
StartAsync
包含启动后台任务的逻辑。 在以下操作之前调用StartAsync
:- 已配置应用的请求处理管道 (
Startup.Configure
)。 - 已启动服务器且已触发 IApplicationLifetime.ApplicationStarted。
可以更改默认行为,以便在配置应用的管道并调用
ApplicationStarted
之后,运行托管服务的StartAsync
。 若要更改默认行为,请在调用ConfigureWebHostDefaults
后添加托管服务(以下示例中的VideosWatcher
):using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }) .ConfigureServices(services => { services.AddHostedService<VideosWatcher>(); }); }
- 已配置应用的请求处理管道 (
StopAsync(CancellationToken) – 主机正常关闭时触发。
StopAsync
包含结束后台任务的逻辑。 实现 IDisposable 和终结器(析构函数)以处置任何非托管资源。默认情况下,取消令牌会有五秒超时,以指示关闭进程不再正常。 在令牌上请求取消时:
- 应中止应用正在执行的任何剩余后台操作。
StopAsync
中调用的任何方法都应及时返回。
但是,在请求取消后,将不会放弃任务 — 调用方等待所有任务完成。
如果应用意外关闭(例如,应用的进程失败),则可能不会调用
StopAsync
。 因此,在StopAsync
中执行的任何方法或操作都可能不会发生。若要延长默认值为 5 秒的关闭超时值,请设置:
- ShutdownTimeout(当使用通用主机时)。 有关详细信息,请参阅 .NET 通用主机。
- 使用 Web 主机时为关闭超时值主机配置设置。 有关详细信息,请参阅 ASP.NET Core Web 主机。
托管服务在应用启动时激活一次,在应用关闭时正常关闭。 如果在执行后台任务期间引发错误,即使未调用 StopAsync
,也应调用 Dispose
。
BackgroundService 基类
BackgroundService 是用于实现长时间运行的 IHostedService 的基类。
调用 ExecuteAsync(CancellationToken) 来运行后台服务。 实现返回一个 Task,其表示后台服务的整个生存期。 在 ExecuteAsync 变为异步(例如通过调用 await
)之前,不会启动任何其他服务。 避免在 ExecuteAsync
中执行长时间的阻塞初始化工作。 StopAsync(CancellationToken) 中的主机块等待完成 ExecuteAsync
。
调用 IHostedService.StopAsync 时,将触发取消令牌。 当激发取消令牌以便正常关闭服务时,ExecuteAsync
的实现应立即完成。 否则,服务将在关闭超时后不正常关闭。 有关更多信息,请参阅 IHostedService interface 部分。
计时的后台任务
定时后台任务使用 System.Threading.Timer 类。 计时器触发任务的 DoWork
方法。 在 StopAsync
上禁用计时器,并在 Dispose
上处置服务容器时处置计时器:
public class TimedHostedService : IHostedService, IDisposable { private int executionCount = 0; private readonly ILogger<TimedHostedService> _logger; private Timer _timer; public TimedHostedService(ILogger<TimedHostedService> logger) { _logger = logger; } public Task StartAsync(CancellationToken stoppingToken) { _logger.LogInformation("Timed Hosted Service running."); _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5)); return Task.CompletedTask; } private void DoWork(object state) { var count = Interlocked.Increment(ref executionCount); _logger.LogInformation( "Timed Hosted Service is working. Count: {Count}", count); } public Task StopAsync(CancellationToken stoppingToken) { _logger.LogInformation("Timed Hosted Service is stopping."); _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } public void Dispose() { _timer?.Dispose(); } }
Timer 不等待先前的 DoWork
执行完成,因此所介绍的方法可能并不适用于所有场景。 使用 Interlocked.Increment 以原子操作的形式将执行计数器递增,这可确保多个线程不会并行更新 executionCount
。
已使用 AddHostedService
扩展方法在 IHostBuilder.ConfigureServices
(Program.cs) 中注册该服务:
services.AddHostedService<TimedHostedService>();
在后台任务中使用有作用域的服务
要在 BackgroundService 中使用有作用域的服务,请创建作用域。 默认情况下,不会为托管服务创建作用域。
作用域后台任务服务包含后台任务的逻辑。 如下示例中:
- 服务是异步的。
DoWork
方法返回Task
。 出于演示目的,在DoWork
方法中等待 10 秒的延迟。 - ILogger 注入到服务中。
internal interface IScopedProcessingService { Task DoWork(CancellationToken stoppingToken); } internal class ScopedProcessingService : IScopedProcessingService { private int executionCount = 0; private readonly ILogger _logger; public ScopedProcessingService(ILogger<ScopedProcessingService> logger) { _logger = logger; } public async Task DoWork(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { executionCount++; _logger.LogInformation( "Scoped Processing Service is working. Count: {Count}", executionCount); await Task.Delay(10000, stoppingToken); } } }
托管服务创建一个作用域来解决作用域后台任务服务以调用其 DoWork
方法。 DoWork
返回 ExecuteAsync
等待的 Task
:
public class ConsumeScopedServiceHostedService : BackgroundService { private readonly ILogger<ConsumeScopedServiceHostedService> _logger; public ConsumeScopedServiceHostedService(IServiceProvider services, ILogger<ConsumeScopedServiceHostedService> logger) { Services = services; _logger = logger; } public IServiceProvider Services { get; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation( "Consume Scoped Service Hosted Service running."); await DoWork(stoppingToken); } private async Task DoWork(CancellationToken stoppingToken) { _logger.LogInformation( "Consume Scoped Service Hosted Service is working."); using (var scope = Services.CreateScope()) { var scopedProcessingService = scope.ServiceProvider .GetRequiredService<IScopedProcessingService>(); await scopedProcessingService.DoWork(stoppingToken); } } public override async Task StopAsync(CancellationToken stoppingToken) { _logger.LogInformation( "Consume Scoped Service Hosted Service is stopping."); await Task.CompletedTask; } }
已在 IHostBuilder.ConfigureServices
(Program.cs) 中注册这些服务。 已使用 AddHostedService
扩展方法注册托管服务:
services.AddHostedService<ConsumeScopedServiceHostedService>(); services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
排队的后台任务
后台任务队列基于 .NET 4.x QueueBackgroundWorkItem(暂定为 ASP.NET Core 内置版本):
public interface IBackgroundTaskQueue { void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem); Task<Func<CancellationToken, Task>> DequeueAsync( CancellationToken cancellationToken); } public class BackgroundTaskQueue : IBackgroundTaskQueue { private ConcurrentQueue<Func<CancellationToken, Task>> _workItems = new ConcurrentQueue<Func<CancellationToken, Task>>(); private SemaphoreSlim _signal = new SemaphoreSlim(0); public void QueueBackgroundWorkItem( Func<CancellationToken, Task> workItem) { if (workItem == null) { throw new ArgumentNullException(nameof(workItem)); } _workItems.Enqueue(workItem); _signal.Release(); } public async Task<Func<CancellationToken, Task>> DequeueAsync( CancellationToken cancellationToken) { await _signal.WaitAsync(cancellationToken); _workItems.TryDequeue(out var workItem); return workItem; } }
在以下 QueueHostedService
示例中:
BackgroundProcessing
方法返回ExecuteAsync
中等待的Task
。- 在
BackgroundProcessing
中,取消排队并执行队列中的后台任务。 - 服务在
StopAsync
中停止之前,将等待工作项。
public class QueuedHostedService : BackgroundService { private readonly ILogger<QueuedHostedService> _logger; public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILogger<QueuedHostedService> logger) { TaskQueue = taskQueue; _logger = logger; } public IBackgroundTaskQueue TaskQueue { get; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation( $"Queued Hosted Service is running.{Environment.NewLine}" + $"{Environment.NewLine}Tap W to add a work item to the " + $"background queue.{Environment.NewLine}"); await BackgroundProcessing(stoppingToken); } private async Task BackgroundProcessing(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var workItem = await TaskQueue.DequeueAsync(stoppingToken); try { await workItem(stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "Error occurred executing {WorkItem}.", nameof(workItem)); } } } public override async Task StopAsync(CancellationToken stoppingToken) { _logger.LogInformation("Queued Hosted Service is stopping."); await base.StopAsync(stoppingToken); } }
每当在输入设备上选择 w
键时,MonitorLoop
服务将处理托管服务的排队任务:
IBackgroundTaskQueue
注入到MonitorLoop
服务中。- 调用
IBackgroundTaskQueue.QueueBackgroundWorkItem
来将工作项排入队列。 - 工作项模拟长时间运行的后台任务:
- 将执行三次 5 秒的延迟 (
Task.Delay
)。 - 如果任务已取消,
try-catch
语句将捕获 OperationCanceledException。
- 将执行三次 5 秒的延迟 (
public class MonitorLoop { private readonly IBackgroundTaskQueue _taskQueue; private readonly ILogger _logger; private readonly CancellationToken _cancellationToken; public MonitorLoop(IBackgroundTaskQueue taskQueue, ILogger<MonitorLoop> logger, IHostApplicationLifetime applicationLifetime) { _taskQueue = taskQueue; _logger = logger; _cancellationToken = applicationLifetime.ApplicationStopping; } public void StartMonitorLoop() { _logger.LogInformation("Monitor Loop is starting."); // Run a console user input loop in a background thread Task.Run(() => Monitor()); } public void Monitor() { while (!_cancellationToken.IsCancellationRequested) { var keyStroke = Console.ReadKey(); if (keyStroke.Key == ConsoleKey.W) { // Enqueue a background work item _taskQueue.QueueBackgroundWorkItem(async token => { // Simulate three 5-second tasks to complete // for each enqueued work item int delayLoop = 0; var guid = Guid.NewGuid().ToString(); _logger.LogInformation( "Queued Background Task {Guid} is starting.", guid); while (!token.IsCancellationRequested && delayLoop < 3) { try { await Task.Delay(TimeSpan.FromSeconds(5), token); } catch (OperationCanceledException) { // Prevent throwing if the Delay is cancelled } delayLoop++; _logger.LogInformation( "Queued Background Task {Guid} is running. " + "{DelayLoop}/3", guid, delayLoop); } if (delayLoop == 3) { _logger.LogInformation( "Queued Background Task {Guid} is complete.", guid); } else { _logger.LogInformation( "Queued Background Task {Guid} was cancelled.", guid); } }); } } } }
已在 IHostBuilder.ConfigureServices
(Program.cs) 中注册这些服务。 已使用 AddHostedService
扩展方法注册托管服务:
services.AddSingleton<MonitorLoop>(); services.AddHostedService<QueuedHostedService>(); services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
已在 Program.Main
中启动 MontiorLoop
:
var monitorLoop = host.Services.GetRequiredService<MonitorLoop>(); monitorLoop.StartMonitorLoop();
在 ASP.NET Core 中,后台任务作为托管服务实现 。 托管服务是一个类,具有实现 IHostedService 接口的后台任务逻辑。 本主题提供了三个托管服务示例:
- 在计时器上运行的后台任务。
- 激活有作用域的服务的托管服务。 有作用域的服务可使用依赖项注入 (DI)
- 按顺序运行的已排队后台任务。
Package
引用 Microsoft.AspNetCore.App 元包或将包引用添加到 Microsoft.Extensions.Hosting 包。
IHostedService 接口
托管服务实现 IHostedService 接口。 该接口为主机托管的对象定义了两种方法:
StartAsync(CancellationToken) –
StartAsync
包含启动后台任务的逻辑。 当使用 Web 主机时,会在启动服务器并触发 IApplicationLifetime.ApplicationStarted 后调用StartAsync
。 当使用通用主机时,会在触发ApplicationStarted
之前调用StartAsync
。StopAsync(CancellationToken) – 主机正常关闭时触发。
StopAsync
包含结束后台任务的逻辑。 实现 IDisposable 和终结器(析构函数)以处置任何非托管资源。默认情况下,取消令牌会有五秒超时,以指示关闭进程不再正常。 在令牌上请求取消时:
- 应中止应用正在执行的任何剩余后台操作。
StopAsync
中调用的任何方法都应及时返回。
但是,在请求取消后,将不会放弃任务 — 调用方等待所有任务完成。
如果应用意外关闭(例如,应用的进程失败),则可能不会调用
StopAsync
。 因此,在StopAsync
中执行的任何方法或操作都可能不会发生。若要延长默认值为 5 秒的关闭超时值,请设置:
- ShutdownTimeout(当使用通用主机时)。 有关详细信息,请参阅 .NET 通用主机。
- 使用 Web 主机时为关闭超时值主机配置设置。 有关详细信息,请参阅 ASP.NET Core Web 主机。
托管服务在应用启动时激活一次,在应用关闭时正常关闭。 如果在执行后台任务期间引发错误,即使未调用 StopAsync
,也应调用 Dispose
。
计时的后台任务
定时后台任务使用 System.Threading.Timer 类。 计时器触发任务的 DoWork
方法。 在 StopAsync
上禁用计时器,并在 Dispose
上处置服务容器时处置计时器:
internal class TimedHostedService : IHostedService, IDisposable { private readonly ILogger _logger; private Timer _timer; public TimedHostedService(ILogger<TimedHostedService> logger) { _logger = logger; } public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Timed Background Service is starting."); _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(5)); return Task.CompletedTask; } private void DoWork(object state) { _logger.LogInformation("Timed Background Service is working."); } public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Timed Background Service is stopping."); _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } public void Dispose() { _timer?.Dispose(); } }
Timer 不等待先前的 DoWork
执行完成,因此所介绍的方法可能并不适用于所有场景。
已使用 AddHostedService
扩展方法在 Startup.ConfigureServices
中注册该服务:
services.AddHostedService<TimedHostedService>();
在后台任务中使用有作用域的服务
要在 IHostedService
中使用有作用域的服务,请创建一个作用域。 默认情况下,不会为托管服务创建作用域。
作用域后台任务服务包含后台任务的逻辑。 在以下示例中,将 ILogger 注入到服务中:
internal interface IScopedProcessingService { void DoWork(); } internal class ScopedProcessingService : IScopedProcessingService { private readonly ILogger _logger; public ScopedProcessingService(ILogger<ScopedProcessingService> logger) { _logger = logger; } public void DoWork() { _logger.LogInformation("Scoped Processing Service is working."); } }
托管服务创建一个作用域来解决作用域后台任务服务以调用其 DoWork
方法:
internal class ConsumeScopedServiceHostedService : IHostedService { private readonly ILogger _logger; public ConsumeScopedServiceHostedService(IServiceProvider services, ILogger<ConsumeScopedServiceHostedService> logger) { Services = services; _logger = logger; } public IServiceProvider Services { get; } public Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation( "Consume Scoped Service Hosted Service is starting."); DoWork(); return Task.CompletedTask; } private void DoWork() { _logger.LogInformation( "Consume Scoped Service Hosted Service is working."); using (var scope = Services.CreateScope()) { var scopedProcessingService = scope.ServiceProvider .GetRequiredService<IScopedProcessingService>(); scopedProcessingService.DoWork(); } } public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation( "Consume Scoped Service Hosted Service is stopping."); return Task.CompletedTask; } }
已在 Startup.ConfigureServices
中注册这些服务。 已使用 AddHostedService
扩展方法注册 IHostedService
实现:
services.AddHostedService<ConsumeScopedServiceHostedService>(); services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
排队的后台任务
后台任务队列基于 .NET Framework 4.x QueueBackgroundWorkItem(暂定为 ASP.NET Core 内置版本):
public interface IBackgroundTaskQueue { void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem); Task<Func<CancellationToken, Task>> DequeueAsync( CancellationToken cancellationToken); } public class BackgroundTaskQueue : IBackgroundTaskQueue { private ConcurrentQueue<Func<CancellationToken, Task>> _workItems = new ConcurrentQueue<Func<CancellationToken, Task>>(); private SemaphoreSlim _signal = new SemaphoreSlim(0); public void QueueBackgroundWorkItem( Func<CancellationToken, Task> workItem) { if (workItem == null) { throw new ArgumentNullException(nameof(workItem)); } _workItems.Enqueue(workItem); _signal.Release(); } public async Task<Func<CancellationToken, Task>> DequeueAsync( CancellationToken cancellationToken) { await _signal.WaitAsync(cancellationToken); _workItems.TryDequeue(out var workItem); return workItem; } }
在 QueueHostedService
中,队列中的后台任务会取消排队,并作为 BackgroundService 执行,此类是用于实现长时间运行 IHostedService
的基类:
public class QueuedHostedService : BackgroundService { private readonly ILogger _logger; public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILoggerFactory loggerFactory) { TaskQueue = taskQueue; _logger = loggerFactory.CreateLogger<QueuedHostedService>(); } public IBackgroundTaskQueue TaskQueue { get; } protected async override Task ExecuteAsync( CancellationToken cancellationToken) { _logger.LogInformation("Queued Hosted Service is starting."); while (!cancellationToken.IsCancellationRequested) { var workItem = await TaskQueue.DequeueAsync(cancellationToken); try { await workItem(cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "Error occurred executing {WorkItem}.", nameof(workItem)); } } _logger.LogInformation("Queued Hosted Service is stopping."); } }
已在 Startup.ConfigureServices
中注册这些服务。 已使用 AddHostedService
扩展方法注册 IHostedService
实现:
services.AddHostedService<QueuedHostedService>(); services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
在索引页模型类中:
- 将
IBackgroundTaskQueue
注入构造函数并分配给Queue
。 - 注入 IServiceScopeFactory 并将其分配给
_serviceScopeFactory
。 工厂用于创建 IServiceScope 的实例,用于在范围内创建服务。 创建范围是为了使用应用的AppDbContext
(设置了范围的服务),以在IBackgroundTaskQueue
(单一实例服务)中写入数据库记录。
public class IndexModel : PageModel { private readonly AppDbContext _db; private readonly ILogger _logger; private readonly IServiceScopeFactory _serviceScopeFactory; public IndexModel(AppDbContext db, IBackgroundTaskQueue queue, ILogger<IndexModel> logger, IServiceScopeFactory serviceScopeFactory) { _db = db; _logger = logger; Queue = queue; _serviceScopeFactory = serviceScopeFactory; } public IBackgroundTaskQueue Queue { get; }
在索引页上选择“添加任务”按钮时,会执行 OnPostAddTask
方法 。 调用 QueueBackgroundWorkItem
来将工作项排入队列:
public IActionResult OnPostAddTaskAsync() { Queue.QueueBackgroundWorkItem(async token => { var guid = Guid.NewGuid().ToString(); using (var scope = _serviceScopeFactory.CreateScope()) { var scopedServices = scope.ServiceProvider; var db = scopedServices.GetRequiredService<AppDbContext>(); for (int delayLoop = 1; delayLoop < 4; delayLoop++) { try { db.Messages.Add( new Message() { Text = $"Queued Background Task {guid} has " + $"written a step. {delayLoop}/3" }); await db.SaveChangesAsync(); } catch (Exception ex) { _logger.LogError(ex, "An error occurred writing to the " + "database. Error: {Message}", ex.Message); } await Task.Delay(TimeSpan.FromSeconds(5), token); } } _logger.LogInformation( "Queued Background Task {Guid} is complete. 3/3", guid); }); return RedirectToPage(); }