添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

在 ASP.NET Core 中,后台任务作为托管服务实现。 托管服务是一个类,具有实现 IHostedService 接口的后台任务逻辑。 本文提供了三个托管服务示例:

  • 在计时器上运行的后台任务。
  • 激活有 作用域的服务 的托管服务。 有作用域的服务可使用 依赖项注入 (DI)
  • 按顺序运行的已排队后台任务。
  • 辅助角色服务模板

    ASP.NET Core 辅助角色服务模板可作为编写长期服务应用的起点。 通过辅助角色服务模板创建的应用将在其项目文件中指定 Worker SDK:

    <Project Sdk="Microsoft.NET.Sdk.Worker">
    

    要使用该模板作为编写托管服务应用的基础:

    Visual Studio Visual Studio for Mac .NET Core CLI
  • 在侧栏中的“.NET Core”下,选择“应用”
  • 在“ASP.NET Core”下,选择“辅助角色”。 选择“下一页”。
  • 对于“目标框架”,选择“.NET Core 3.1”或更高版本 。 选择“下一页”。
  • 在“项目名称”字段中提供名称。 选择“创建”。
  • 将辅助角色服务 (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 接口

    IHostedService 接口为主机托管的对象定义了两种方法:

  • StartAsync(CancellationToken)
  • StopAsync(CancellationToken)
  • StartAsync

    StartAsync(CancellationToken) 包含用于启动后台任务的逻辑。 在以下操作之前调用 StartAsync

  • 已配置应用的请求处理管道。
  • 已启动服务器且已触发 IApplicationLifetime.ApplicationStarted
  • StartAsync 应仅限于短期任务,因为托管服务是按顺序运行的,在 StartAsync 运行完成之前不会启动其他服务。

    StopAsync

  • StopAsync(CancellationToken) 在主机执行正常关闭时触发。 StopAsync 包含结束后台任务的逻辑。 实现 IDisposable终结器(析构函数)以处置任何非托管资源。
  • 默认情况下,取消令牌会有 30 秒超时,以指示关闭进程不再正常。 在令牌上请求取消时:

  • 应中止应用正在执行的任何剩余后台操作。
  • StopAsync 中调用的任何方法都应及时返回。
  • 但是,在请求取消后,将不会放弃任务,调用方会等待所有任务完成。

    如果应用意外关闭(例如,应用的进程失败),则可能不会调用 StopAsync。 因此,在 StopAsync 中执行的任何方法或操作都可能不会发生。

    若要延长默认值为 30 秒的关闭超时值,请设置:

  • ShutdownTimeout(当使用通用主机时)。 有关详细信息,请参阅 ASP.NET Core 中的 .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 部分。

    有关详细信息,请参阅 BackgroundService 源代码。

    计时的后台任务

    定时后台任务使用 System.Threading.Timer 类。 计时器触发任务的 DoWork 方法。 在 StopAsync 上禁用计时器,并在 Dispose 上处置服务容器时处置计时器:

    public class TimedHostedService : IHostedService, IDisposable private int executionCount = 0; private readonly ILogger<TimedHostedService> _logger; private Timer? _timer = null; 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 base.StopAsync(stoppingToken);

    已在 IHostBuilder.ConfigureServices (Program.cs) 中注册这些服务。 已使用 AddHostedService 扩展方法注册托管服务:

    services.AddHostedService<ConsumeScopedServiceHostedService>(); services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

    排队的后台任务

    后台任务队列基于 .NET 4.x QueueBackgroundWorkItem

    public interface IBackgroundTaskQueue ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem); ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync( CancellationToken cancellationToken); public class BackgroundTaskQueue : IBackgroundTaskQueue private readonly Channel<Func<CancellationToken, ValueTask>> _queue; public BackgroundTaskQueue(int capacity) // Capacity should be set based on the expected application load and // number of concurrent threads accessing the queue. // BoundedChannelFullMode.Wait will cause calls to WriteAsync() to return a task, // which completes only when space became available. This leads to backpressure, // in case too many publishers/calls start accumulating. var options = new BoundedChannelOptions(capacity) FullMode = BoundedChannelFullMode.Wait _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options); public async ValueTask QueueBackgroundWorkItemAsync( Func<CancellationToken, ValueTask> workItem) if (workItem == null) throw new ArgumentNullException(nameof(workItem)); await _queue.Writer.WriteAsync(workItem); public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync( CancellationToken cancellationToken) var workItem = await _queue.Reader.ReadAsync(cancellationToken); 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); 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
    • 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("MonitorAsync Loop is starting."); // Run a console user input loop in a background thread Task.Run(async () => await MonitorAsync()); private async ValueTask MonitorAsync() while (!_cancellationToken.IsCancellationRequested) var keyStroke = Console.ReadKey(); if (keyStroke.Key == ConsoleKey.W) // Enqueue a background work item await _taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem); private async ValueTask BuildWorkItem(CancellationToken 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) 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); _logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);

      已在 IHostBuilder.ConfigureServices (Program.cs) 中注册这些服务。 已使用 AddHostedService 扩展方法注册托管服务:

      services.AddSingleton<MonitorLoop>(); services.AddHostedService<QueuedHostedService>(); services.AddSingleton<IBackgroundTaskQueue>(ctx => if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity)) queueCapacity = 100; return new BackgroundTaskQueue(queueCapacity);

      已在 Program.cs 中启动 MonitorLoop

      var monitorLoop = host.Services.GetRequiredService<MonitorLoop>(); monitorLoop.StartMonitorLoop();

      异步定时后台任务

      以下代码将创建异步定时后台任务:

      namespace TimedBackgroundTasks; public class TimedHostedService : BackgroundService private readonly ILogger<TimedHostedService> _logger; private int _executionCount; public TimedHostedService(ILogger<TimedHostedService> logger) _logger = logger; protected override async Task ExecuteAsync(CancellationToken stoppingToken) _logger.LogInformation("Timed Hosted Service running."); // When the timer should have no due-time, then do the work once now. DoWork(); using PeriodicTimer timer = new(TimeSpan.FromSeconds(1)); while (await timer.WaitForNextTickAsync(stoppingToken)) DoWork(); catch (OperationCanceledException) _logger.LogInformation("Timed Hosted Service is stopping."); // Could also be a async method, that can be awaited in ExecuteAsync above private void DoWork() int count = Interlocked.Increment(ref _executionCount); _logger.LogInformation("Timed Hosted Service is working. Count: {Count}", count);

      本机 AOT

      辅助角色服务模板支持带有 --aot 标志的 .NET 本机预先 (AOT)

      Visual Studio .NET Core CLI <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <InvariantGlobalization>true</InvariantGlobalization> + <PublishAot>true</PublishAot> <UserSecretsId>dotnet-WorkerWithAot-e94b2</UserSecretsId> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.4.23259.5" /> </ItemGroup> </Project>
    • GitHub 上的后台服务单元测试
    • 查看或下载示例代码如何下载
    • 使用 IHostedService 和 BackgroundService 类在微服务中实现后台任务
    • 在 Azure 应用服务中使用 WebJobs 运行后台任务
    • Timer
    • 在 ASP.NET Core 中,后台任务作为托管服务实现。 托管服务是一个类,具有实现 IHostedService 接口的后台任务逻辑。 本文提供了三个托管服务示例:

    • 在计时器上运行的后台任务。
    • 激活有作用域的服务的托管服务。 有作用域的服务可使用依赖项注入 (DI)
    • 按顺序运行的已排队后台任务。
    • 辅助角色服务模板

      ASP.NET Core 辅助角色服务模板可作为编写长期服务应用的起点。 通过辅助角色服务模板创建的应用将在其项目文件中指定 Worker SDK:

      <Project Sdk="Microsoft.NET.Sdk.Worker">
      

      要使用该模板作为编写托管服务应用的基础:

      Visual Studio Visual Studio for Mac .NET Core CLI
    • 在侧栏中的“.NET Core”下,选择“应用”
    • 在“ASP.NET Core”下,选择“辅助角色”。 选择“下一页”。
    • 对于“目标框架”,选择“.NET Core 3.1”或更高版本 。 选择“下一页”。
    • 在“项目名称”字段中提供名称。 选择“创建”。
    • 将辅助角色服务 (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 接口

      IHostedService 接口为主机托管的对象定义了两种方法:

    • StartAsync(CancellationToken)
    • StopAsync(CancellationToken)
    • StartAsync

      StartAsync(CancellationToken) 包含用于启动后台任务的逻辑。 在以下操作之前调用 StartAsync

    • 已配置应用的请求处理管道。
    • 已启动服务器且已触发 IApplicationLifetime.ApplicationStarted
    • StartAsync 应仅限于短期任务,因为托管服务是按顺序运行的,在 StartAsync 运行完成之前不会启动其他服务。

      StopAsync

    • StopAsync(CancellationToken) 在主机执行正常关闭时触发。 StopAsync 包含结束后台任务的逻辑。 实现 IDisposable终结器(析构函数)以处置任何非托管资源。
    • 默认情况下,取消令牌会有 30 秒超时,以指示关闭进程不再正常。 在令牌上请求取消时:

    • 应中止应用正在执行的任何剩余后台操作。
    • StopAsync 中调用的任何方法都应及时返回。
    • 但是,在请求取消后,将不会放弃任务,调用方会等待所有任务完成。

      如果应用意外关闭(例如,应用的进程失败),则可能不会调用 StopAsync。 因此,在 StopAsync 中执行的任何方法或操作都可能不会发生。

      若要延长默认值为 30 秒的关闭超时值,请设置:

    • ShutdownTimeout(当使用通用主机时)。 有关详细信息,请参阅 ASP.NET Core 中的 .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 部分。

      有关详细信息,请参阅 BackgroundService 源代码。

      计时的后台任务

      定时后台任务使用 System.Threading.Timer 类。 计时器触发任务的 DoWork 方法。 在 StopAsync 上禁用计时器,并在 Dispose 上处置服务容器时处置计时器:

      public class TimedHostedService : IHostedService, IDisposable private int executionCount = 0; private readonly ILogger<TimedHostedService> _logger; private Timer? _timer = null; 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 base.StopAsync(stoppingToken);

      已在 IHostBuilder.ConfigureServices (Program.cs) 中注册这些服务。 已使用 AddHostedService 扩展方法注册托管服务:

      services.AddHostedService<ConsumeScopedServiceHostedService>(); services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

      排队的后台任务

      后台任务队列基于 .NET 4.x QueueBackgroundWorkItem

      public interface IBackgroundTaskQueue ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem); ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync( CancellationToken cancellationToken); public class BackgroundTaskQueue : IBackgroundTaskQueue private readonly Channel<Func<CancellationToken, ValueTask>> _queue; public BackgroundTaskQueue(int capacity) // Capacity should be set based on the expected application load and // number of concurrent threads accessing the queue. // BoundedChannelFullMode.Wait will cause calls to WriteAsync() to return a task, // which completes only when space became available. This leads to backpressure, // in case too many publishers/calls start accumulating. var options = new BoundedChannelOptions(capacity) FullMode = BoundedChannelFullMode.Wait _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options); public async ValueTask QueueBackgroundWorkItemAsync( Func<CancellationToken, ValueTask> workItem) if (workItem == null) throw new ArgumentNullException(nameof(workItem)); await _queue.Writer.WriteAsync(workItem); public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync( CancellationToken cancellationToken) var workItem = await _queue.Reader.ReadAsync(cancellationToken); 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); 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
      • 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("MonitorAsync Loop is starting."); // Run a console user input loop in a background thread Task.Run(async () => await MonitorAsync()); private async ValueTask MonitorAsync() while (!_cancellationToken.IsCancellationRequested) var keyStroke = Console.ReadKey(); if (keyStroke.Key == ConsoleKey.W) // Enqueue a background work item await _taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem); private async ValueTask BuildWorkItem(CancellationToken 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) 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); _logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);

        已在 IHostBuilder.ConfigureServices (Program.cs) 中注册这些服务。 已使用 AddHostedService 扩展方法注册托管服务:

        services.AddSingleton<MonitorLoop>(); services.AddHostedService<QueuedHostedService>(); services.AddSingleton<IBackgroundTaskQueue>(ctx => if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity)) queueCapacity = 100; return new BackgroundTaskQueue(queueCapacity);

        已在 Program.cs 中启动 MonitorLoop

        var monitorLoop = host.Services.GetRequiredService<MonitorLoop>(); monitorLoop.StartMonitorLoop();

        异步定时后台任务

        以下代码将创建异步定时后台任务:

        namespace TimedBackgroundTasks; public class TimedHostedService : BackgroundService private readonly ILogger<TimedHostedService> _logger; private int _executionCount; public TimedHostedService(ILogger<TimedHostedService> logger) _logger = logger; protected override async Task ExecuteAsync(CancellationToken stoppingToken) _logger.LogInformation("Timed Hosted Service running."); // When the timer should have no due-time, then do the work once now. DoWork(); using PeriodicTimer timer = new(TimeSpan.FromSeconds(1)); while (await timer.WaitForNextTickAsync(stoppingToken)) DoWork(); catch (OperationCanceledException) _logger.LogInformation("Timed Hosted Service is stopping."); // Could also be a async method, that can be awaited in ExecuteAsync above private void DoWork() int count = Interlocked.Increment(ref _executionCount); _logger.LogInformation("Timed Hosted Service is working. Count: {Count}", count);
      • GitHub 上的后台服务单元测试
      • 查看或下载示例代码如何下载
      • 使用 IHostedService 和 BackgroundService 类在微服务中实现后台任务
      • 在 Azure 应用服务中使用 WebJobs 运行后台任务
      • Timer
      • 在 ASP.NET Core 中,后台任务作为托管服务实现。 托管服务是一个类,具有实现 IHostedService 接口的后台任务逻辑。 本文提供了三个托管服务示例:

      • 在计时器上运行的后台任务。
      • 激活有作用域的服务的托管服务。 有作用域的服务可使用依赖项注入 (DI)
      • 按顺序运行的已排队后台任务。
      • 查看或下载示例代码如何下载

        辅助角色服务模板

        ASP.NET Core 辅助角色服务模板可作为编写长期服务应用的起点。 通过辅助角色服务模板创建的应用将在其项目文件中指定 Worker SDK:

        <Project Sdk="Microsoft.NET.Sdk.Worker">
        

        要使用该模板作为编写托管服务应用的基础:

        Visual Studio Visual Studio for Mac .NET Core CLI
      • 在侧栏中的“.NET Core”下,选择“应用”
      • 在“ASP.NET Core”下,选择“辅助角色”。 选择“下一页”。
      • 对于“目标框架”,选择“.NET Core 3.0”或更高版本。 选择“下一页”。
      • 在“项目名称”字段中提供名称。 选择“创建”。
      • 将辅助角色服务 (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 接口

        IHostedService 接口为主机托管的对象定义了两种方法:

      • StartAsync(CancellationToken)
      • StopAsync(CancellationToken)
      • StartAsync

        StartAsync 包含启动后台任务的逻辑。 在以下操作之前调用 StartAsync

      • 已配置应用的请求处理管道。
      • 已启动服务器且已触发 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

      • StopAsync(CancellationToken) 在主机执行正常关闭时触发。 StopAsync 包含结束后台任务的逻辑。 实现 IDisposable终结器(析构函数)以处置任何非托管资源。
      • 默认情况下,取消令牌会有五秒超时,以指示关闭进程不再正常。 在令牌上请求取消时:

      • 应中止应用正在执行的任何剩余后台操作。
      • StopAsync 中调用的任何方法都应及时返回。
      • 但是,在请求取消后,将不会放弃任务,调用方会等待所有任务完成。

        如果应用意外关闭(例如,应用的进程失败),则可能不会调用 StopAsync。 因此,在 StopAsync 中执行的任何方法或操作都可能不会发生。

        若要延长默认值为 5 秒的关闭超时值,请设置:

      • ShutdownTimeout(当使用通用主机时)。 有关详细信息,请参阅 ASP.NET Core 中的 .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 部分。

        StartAsync 应仅限于短期任务,因为托管服务是按顺序运行的,在 StartAsync 运行完成之前不会启动其他服务。 长期任务应放置在 ExecuteAsync 中。 有关详细信息,请参阅 BackgroundService 数据源。

        计时的后台任务

        定时后台任务使用 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 base.StopAsync(stoppingToken);

        已在 IHostBuilder.ConfigureServices (Program.cs) 中注册这些服务。 已使用 AddHostedService 扩展方法注册托管服务:

        services.AddHostedService<ConsumeScopedServiceHostedService>(); services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

        排队的后台任务

        后台任务队列基于 .NET 4.x QueueBackgroundWorkItem

        public interface IBackgroundTaskQueue ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem); ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync( CancellationToken cancellationToken); public class BackgroundTaskQueue : IBackgroundTaskQueue private readonly Channel<Func<CancellationToken, ValueTask>> _queue; public BackgroundTaskQueue(int capacity) // Capacity should be set based on the expected application load and // number of concurrent threads accessing the queue. // BoundedChannelFullMode.Wait will cause calls to WriteAsync() to return a task, // which completes only when space became available. This leads to backpressure, // in case too many publishers/calls start accumulating. var options = new BoundedChannelOptions(capacity) FullMode = BoundedChannelFullMode.Wait _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options); public async ValueTask QueueBackgroundWorkItemAsync( Func<CancellationToken, ValueTask> workItem) if (workItem == null) throw new ArgumentNullException(nameof(workItem)); await _queue.Writer.WriteAsync(workItem); public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync( CancellationToken cancellationToken) var workItem = await _queue.Reader.ReadAsync(cancellationToken); 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); 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
        • 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("MonitorAsync Loop is starting."); // Run a console user input loop in a background thread Task.Run(async () => await MonitorAsync()); private async ValueTask MonitorAsync() while (!_cancellationToken.IsCancellationRequested) var keyStroke = Console.ReadKey(); if (keyStroke.Key == ConsoleKey.W) // Enqueue a background work item await _taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem); private async ValueTask BuildWorkItem(CancellationToken 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) 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); _logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);

          已在 IHostBuilder.ConfigureServices (Program.cs) 中注册这些服务。 已使用 AddHostedService 扩展方法注册托管服务:

          services.AddSingleton<MonitorLoop>(); services.AddHostedService<QueuedHostedService>(); services.AddSingleton<IBackgroundTaskQueue>(ctx => { if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity)) queueCapacity = 100; return new BackgroundTaskQueue(queueCapacity);

          已在 Program.Main 中启动 MonitorLoop

          var monitorLoop = host.Services.GetRequiredService<MonitorLoop>(); monitorLoop.StartMonitorLoop();
        • 使用 IHostedService 和 BackgroundService 类在微服务中实现后台任务
        • 在 Azure 应用服务中使用 WebJobs 运行后台任务
        • Timer
  •