在 ASP.NET Core 中使用托管服务实现后台任务

作者:Luke LathamJeow Li Huan

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

查看或下载示例代码如何下载

辅助角色服务模板

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

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

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

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

    可以更改默认行为,以便在配置应用的管道并调用 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 基类

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 来将工作项排入队列。
  • 工作项模拟长时间运行的后台任务:
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 接口的后台任务逻辑。 本主题提供了三个托管服务示例:

查看或下载示例代码如何下载

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 秒的关闭超时值,请设置:

托管服务在应用启动时激活一次,在应用关闭时正常关闭。 如果在执行后台任务期间引发错误,即使未调用 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();
}

其他资源

上一篇:ASP.NET Core 中 .NET 的开放 Web 接口 (OWIN)

下一篇:在 ASP.NET Core 中使用承载启动程序集

关注微信小程序
程序员编程王-随时随地学编程

扫描二维码
程序员编程王

扫一扫关注最新编程教程