作者:Luke Latham 和 Jeow Li Huan
在 ASP.NET Core 中,后台任务作为托管服务实现 。 托管服务是一个类,具有实现 IHostedService 接口的后台任务逻辑。 本主题提供了三个托管服务示例:
ASP.NET Core 辅助角色服务模板可作为编写长期服务应用的起点。 通过辅助角色服务模板创建的应用将在其项目文件中指定 Worker SDK:
<Project Sdk="Microsoft.NET.Sdk.Worker">
要使用该模板作为编写托管服务应用的基础:
将辅助角色服务 (worker
) 模板用于命令行界面中的 dotnet new 命令。 下面的示例中创建了名为 ContosoWorker
的辅助角色服务应用。 执行命令时会自动为 ContosoWorker
应用创建文件夹。
dotnet new worker -o ContosoWorker
基于辅助角色服务模板的应用使用 Microsoft.NET.Sdk.Worker
SDK,并且具有对 Microsoft.Extensions.Hosting 包的显式包引用。 有关示例,请参阅示例应用的项目文件 (BackgroundTasksSample.csproj)。
对于使用 Microsoft.NET.Sdk.Web
SDK 的 Web 应用,通过共享框架隐式引用 Microsoft.Extensions.Hosting 包。 在应用的项目文件中不需要显式包引用。
IHostedService 接口为主机托管的对象定义了两种方法:
StartAsync(CancellationToken) – StartAsync
包含启动后台任务的逻辑。 在以下操作之前调用 StartAsync
:
Startup.Configure
)。可以更改默认行为,以便在配置应用的管道并调用 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 秒的关闭超时值,请设置:
托管服务在应用启动时激活一次,在应用关闭时正常关闭。 如果在执行后台任务期间引发错误,即使未调用 StopAsync
,也应调用 Dispose
。
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 秒的延迟。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
来将工作项排入队列。Task.Delay
)。try-catch
语句将捕获 OperationCanceledException。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 接口的后台任务逻辑。 本主题提供了三个托管服务示例:
引用 Microsoft.AspNetCore.App 元包或将包引用添加到 Microsoft.Extensions.Hosting 包。
托管服务实现 IHostedService 接口。 该接口为主机托管的对象定义了两种方法:
StartAsync(CancellationToken) – StartAsync
包含启动后台任务的逻辑。 当使用 Web 主机时,会在启动服务器并触发 IApplicationLifetime.ApplicationStarted 后调用 StartAsync
。 当使用通用主机时,会在触发 ApplicationStarted
之前调用 StartAsync
。
StopAsync(CancellationToken) – 主机正常关闭时触发。 StopAsync
包含结束后台任务的逻辑。 实现 IDisposable 和终结器(析构函数)以处置任何非托管资源。
默认情况下,取消令牌会有五秒超时,以指示关闭进程不再正常。 在令牌上请求取消时:
StopAsync
中调用的任何方法都应及时返回。但是,在请求取消后,将不会放弃任务 — 调用方等待所有任务完成。
如果应用意外关闭(例如,应用的进程失败),则可能不会调用 StopAsync
。 因此,在 StopAsync
中执行的任何方法或操作都可能不会发生。
若要延长默认值为 5 秒的关闭超时值,请设置:
托管服务在应用启动时激活一次,在应用关闭时正常关闭。 如果在执行后台任务期间引发错误,即使未调用 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
。_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(); }