在 ASP.NET Core 中使用託管服務的背景工作

Jeow Li Huan

在 ASP.NET Core 中,背景工作可實作為「託管服務」。 託管服務是具有背景工作邏輯的類別,可實作 IHostedService 介面。 本文提供三個託管服務範例:

  • 在計時器上執行的背景工作。
  • 啟用 範圍服務的託管服務。 已設定範圍的服務可以使用 (DI) 的相依性插入。
  • 以循序方式執行的排入佇列背景工作。

查看或下載範例程式碼 (如何下載)

背景工作服務範本

ASP.NET Core 背景工作服務範本提供撰寫長期執行服務應用程式的起點。 從背景工作服務範本建立的應用程式會在其專案檔中指定工作者 SDK:

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

使用範本作為裝載服務應用程式的基礎:

  1. 建立新專案。
  2. 選取 [背景 工作服務]。 選取 [下一步] 。
  3. 在 [專案名稱] 欄位中提供專案名稱,或接受預設專案名稱。 選取 [下一步] 。
  4. 在 [ 其他資訊 ] 對話方塊中,選擇 架構。 選取 [建立]。

套件

以背景工作角色服務範本為基礎的應用程式會使用 Microsoft.NET.Sdk.Worker SDK,而且會有明確的套件參考,可參考至 裝載 套件。 例如,請參閱範例應用程式的專案檔 (BackgroundTasksSample .csproj) 。

針對使用 SDK 的 web 應用程式 Microsoft.NET.Sdk.Web ,會以隱含方式從共用架構參考 裝載 套件。 應用程式專案檔中不需要明確的套件參考。

IHostedService 介面

IHostedService介面會為主機所管理的物件定義兩種方法:

StartAsync

StartAsync 包含用來啟動背景工作的邏輯。 StartAsync在之前 呼叫:

您可以變更預設行為,讓託管服務在 StartAsync 應用程式的管線設定完成之後執行,然後 ApplicationStarted 呼叫。 若要變更預設行為,請 VideosWatcher 在呼叫之後,在下列範例) 中加入託管服務 (CreateBuilder

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddHostedService<VideosWatcher>();}

StopAsync

取消權杖有五秒的逾時預設值,以表示關機程序應該不再順利。 在權杖上要求取消時:

  • 應終止應用程式正在執行的任何剩餘背景作業。
  • StopAsync 中呼叫的任何方法應立即傳回。

不過,不會在要求取消後直接放棄工作—呼叫者會等待所有工作完成。

如果應用程式意外關閉 (例如,應用程式的處理序失敗),可能不會呼叫 StopAsync。 因此,任何在 StopAsync 中所呼叫方法或所執行作業可能不會發生。

若要延長預設的五秒鐘關機逾時,請設定:

託管服務會在應用程式啟動時隨即啟動,然後在應用程式關閉時正常關閉。 如果在背景工作執行期間擲回錯誤,即使未呼叫 StopAsync,也應該呼叫 Dispose

BackgroundService 基類

BackgroundService 是用來執行長時間執行的基類 IHostedService

呼叫ExecuteAsync (CancellationToken) ,以執行背景服務。 執行 Task 會傳回,代表背景服務的整個存留期。 在 ExecuteAsync 變成非同步之前,不會啟動任何其他服務,例如藉由呼叫 await 。 避免執行長時間的封鎖初始化工作 ExecuteAsync 。 StopAsync 中的主機區塊 (CancellationToken) 等候 ExecuteAsync 完成。

呼叫 IHostedService 時,會觸發解除標記。 您應在 ExecuteAsync 引發解除標記時立即完成您的執行,以便正常地關閉服務。 否則,服務強制會在關閉超時時關機。 如需詳細資訊,請參閱 IHostedService 介面 一節。

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 = 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 完成,因此所顯示的方法可能不適合每種案例。 連鎖。遞增 是用來將執行計數器遞增為不可部分完成的作業,以確保多個執行緒不會 executionCount 同時更新。

服務會在 IHostBuilder.ConfigureServices (Program) 中註冊,並使用 AddHostedService 擴充方法:

services.AddHostedService<TimedHostedService>();

在背景工作中使用範圍服務

若要在BackgroundService中使用範圍服務,請建立一個範圍。 根據預設,不會針對託管服務建立任何範圍。

範圍背景工作服務包含背景工作的邏輯。 在下例中︰

  • 服務是非同步。 DoWork 方法會傳回 Task。 基於示範目的,方法中會等待10秒的延遲 DoWork
  • 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 傳回 Task ,它會在中等待 ExecuteAsync

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) 中註冊。 託管服務是使用 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方法 Task 會傳回在中等候的 ExecuteAsync
  • 佇列中的背景工作會從佇列中清除並執行 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);
    }
}

MonitorLoop每當在 w 輸入裝置上選取索引鍵時,服務會處理託管服務的佇列工作:

  • IBackgroundTaskQueue會插入 MonitorLoop 服務中。
  • IBackgroundTaskQueue.QueueBackgroundWorkItem 呼叫以將工作專案排入佇列。
  • 工作專案會模擬長時間執行的背景工作:
    • 3 5-秒的延遲是 (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("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)
        {
            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) 中註冊。 託管服務是使用 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);
});

MonitorLoop 會在 程式 .cs 中啟動:

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

其他資源

在 ASP.NET Core 中,背景工作可實作為「託管服務」。 託管服務是具有背景工作邏輯的類別,可實作 IHostedService 介面。 本文提供三個託管服務範例:

  • 在計時器上執行的背景工作。
  • 啟用 範圍服務的託管服務。 已設定範圍的服務可以使用 (DI) 的相依性插入。
  • 以循序方式執行的排入佇列背景工作。

查看或下載範例程式碼 (如何下載)

背景工作服務範本

ASP.NET Core 背景工作服務範本提供撰寫長期執行服務應用程式的起點。 從背景工作服務範本建立的應用程式會在其專案檔中指定工作者 SDK:

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

使用範本作為裝載服務應用程式的基礎:

  1. 建立新專案。
  2. 選取 [背景 工作服務]。 選取 [下一步] 。
  3. 在 [專案名稱] 欄位中提供專案名稱,或接受預設專案名稱。 選取 [建立]。
  4. 在 [ 建立新的背景工作服務 ] 對話方塊中,選取 [ 建立]。

套件

以背景工作角色服務範本為基礎的應用程式會使用 Microsoft.NET.Sdk.Worker SDK,而且會有明確的套件參考,可參考至 裝載 套件。 例如,請參閱範例應用程式的專案檔 (BackgroundTasksSample .csproj) 。

針對使用 SDK 的 web 應用程式 Microsoft.NET.Sdk.Web ,會以隱含方式從共用架構參考 裝載 套件。 應用程式專案檔中不需要明確的套件參考。

IHostedService 介面

IHostedService介面會為主機所管理的物件定義兩種方法:

StartAsync

StartAsync 包含用來啟動背景工作的邏輯。 StartAsync在之前 呼叫:

您可以變更預設行為,讓託管服務在 StartAsync 應用程式的管線設定完成之後執行,然後 ApplicationStarted 呼叫。 若要變更預設行為,請 VideosWatcher 在呼叫之後,在下列範例) 中加入託管服務 (ConfigureWebHostDefaults

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 中呼叫的任何方法應立即傳回。

不過,不會在要求取消後直接放棄工作—呼叫者會等待所有工作完成。

如果應用程式意外關閉 (例如,應用程式的處理序失敗),可能不會呼叫 StopAsync。 因此,任何在 StopAsync 中所呼叫方法或所執行作業可能不會發生。

若要延長預設的五秒鐘關機逾時,請設定:

託管服務會在應用程式啟動時隨即啟動,然後在應用程式關閉時正常關閉。 如果在背景工作執行期間擲回錯誤,即使未呼叫 StopAsync,也應該呼叫 Dispose

BackgroundService 基類

BackgroundService 是用來執行長時間執行的基類 IHostedService

呼叫ExecuteAsync (CancellationToken) ,以執行背景服務。 執行 Task 會傳回,代表背景服務的整個存留期。 在 ExecuteAsync 變成非同步之前,不會啟動任何其他服務,例如藉由呼叫 await 。 避免執行長時間的封鎖初始化工作 ExecuteAsync 。 StopAsync 中的主機區塊 (CancellationToken) 等候 ExecuteAsync 完成。

呼叫 IHostedService 時,會觸發解除標記。 您應在 ExecuteAsync 引發解除標記時立即完成您的執行,以便正常地關閉服務。 否則,服務強制會在關閉超時時關機。 如需詳細資訊,請參閱 IHostedService 介面 一節。

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 完成,因此所顯示的方法可能不適合每種案例。 連鎖。遞增 是用來將執行計數器遞增為不可部分完成的作業,以確保多個執行緒不會 executionCount 同時更新。

服務會在 IHostBuilder.ConfigureServices (Program) 中註冊,並使用 AddHostedService 擴充方法:

services.AddHostedService<TimedHostedService>();

在背景工作中使用範圍服務

若要在BackgroundService中使用範圍服務,請建立一個範圍。 根據預設,不會針對託管服務建立任何範圍。

範圍背景工作服務包含背景工作的邏輯。 在下例中︰

  • 服務是非同步。 DoWork 方法會傳回 Task。 基於示範目的,方法中會等待10秒的延遲 DoWork
  • 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 傳回 Task ,它會在中等待 ExecuteAsync

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) 中註冊。 託管服務是使用 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方法 Task 會傳回在中等候的 ExecuteAsync
  • 佇列中的背景工作會從佇列中清除並執行 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);
    }
}

MonitorLoop每當在 w 輸入裝置上選取索引鍵時,服務會處理託管服務的佇列工作:

  • IBackgroundTaskQueue會插入 MonitorLoop 服務中。
  • IBackgroundTaskQueue.QueueBackgroundWorkItem 呼叫以將工作專案排入佇列。
  • 工作專案會模擬長時間執行的背景工作:
    • 3 5-秒的延遲是 (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("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)
        {
            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) 中註冊。 託管服務是使用 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);
});

MonitorLoop 啟動時間 Program.Main

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

其他資源