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

作者 :Jeow Li Source

注意

這不是本文的最新版本。 如需目前版本,請參閱 本文的 .NET 7 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前版本,請參閱 本文的 .NET 7 版本

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

背景工作服務範本

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

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

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

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

套件

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

對於使用 Microsoft.NET.Sdk.Web SDK 的 Web 應用程式,會從共用架構隱含參考 Microsoft.Extensions.Hosting 套件。 不需要應用程式專案檔中的明確套件參考。

IHostedService 介面

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

StartAsync

StartAsync(CancellationToken) 包含啟動背景工作的邏輯。 StartAsync之前呼叫 :

StartAsync 應該限制為短期執行的工作,因為託管服務會循序執行,而且在執行完成之前 StartAsync 不會啟動任何進一步的服務。

StopAsync

取消權杖有預設的 30 秒逾時,表示關機程式不應該再正常運作。 在權杖上要求取消時:

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

不過,要求取消之後不會放棄工作,呼叫端會等候所有工作完成。

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

若要擴充預設的 30 秒關機逾時,請設定:

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

BackgroundService 基類

BackgroundService 是實作長時間執行的 IHostedService 基類。

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

呼叫 IHostedService.StopAsync 時,就會觸發取消權杖。 您的 實作 ExecuteAsync 應該會在引發取消權杖時立即完成,以便正常關閉服務。 否則,服務會在關機逾時不正常地關閉。 如需詳細資訊,請參閱 IHostedService 介面 一節。

如需詳細資訊,請參閱 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

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

services.AddHostedService<TimedHostedService>();

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

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

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

  • 服務為非同步。 DoWork 方法會傳回 Task。 基於示範用途,在 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 會傳回在 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);
    }
}

這些服務會在 (Program.cs) 中 IHostBuilder.ConfigureServices 註冊。 託管服務會向 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 範例中:

  • 方法會 BackgroundProcessingTask 回 中等候的 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);
    }
}

每次在輸入裝置上選取 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("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);
        }
    }
}

這些服務會在 (Program.cs) 中 IHostBuilder.ConfigureServices 註冊。 託管服務會向 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.cs 啟動:

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));

        try
        {
            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

背景工作服務範本支援 .NET 原生預先 (AOT) --aot 標:

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

AOT 選項會新增 <PublishAot>true</PublishAot> 至專案檔:


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

  <PropertyGroup>
    <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>

其他資源

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

背景工作服務範本

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

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

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

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

套件

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

對於使用 Microsoft.NET.Sdk.Web SDK 的 Web 應用程式,會從共用架構隱含參考 Microsoft.Extensions.Hosting 套件。 不需要應用程式專案檔中的明確套件參考。

IHostedService 介面

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

StartAsync

StartAsync(CancellationToken) 包含啟動背景工作的邏輯。 StartAsync之前呼叫 :

StartAsync 應該限制為短期執行的工作,因為託管服務會循序執行,而且在執行完成之前 StartAsync 不會啟動任何進一步的服務。

StopAsync

取消權杖有預設的 30 秒逾時,表示關機程式不應該再正常運作。 在權杖上要求取消時:

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

不過,要求取消之後不會放棄工作,呼叫端會等候所有工作完成。

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

若要擴充預設的 30 秒關機逾時,請設定:

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

BackgroundService 基類

BackgroundService 是實作長時間執行的 IHostedService 基類。

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

呼叫 IHostedService.StopAsync 時,就會觸發取消權杖。 您的 實作 ExecuteAsync 應該會在引發取消權杖時立即完成,以便正常關閉服務。 否則,服務會在關機逾時不正常地關閉。 如需詳細資訊,請參閱 IHostedService 介面 一節。

如需詳細資訊,請參閱 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

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

services.AddHostedService<TimedHostedService>();

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

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

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

  • 服務為非同步。 DoWork 方法會傳回 Task。 基於示範用途,在 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 會傳回在 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);
    }
}

這些服務會在 (Program.cs) 中 IHostBuilder.ConfigureServices 註冊。 託管服務會向 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 範例中:

  • 方法會 BackgroundProcessingTask 回 中等候的 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);
    }
}

每次在輸入裝置上選取 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("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);
        }
    }
}

這些服務會在 (Program.cs) 中 IHostBuilder.ConfigureServices 註冊。 託管服務會向 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.cs 啟動:

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));

        try
        {
            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);
    }
}

其他資源

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

檢視或下載範例程式碼 \(英文\) (如何下載)

背景工作服務範本

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

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

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

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

套件

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

對於使用 Microsoft.NET.Sdk.Web SDK 的 Web 應用程式,會從共用架構隱含參考 Microsoft.Extensions.Hosting 套件。 不需要應用程式專案檔中的明確套件參考。

IHostedService 介面

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

StartAsync

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

預設行為可以變更,讓託管服務在 StartAsync 設定應用程式管線 ApplicationStarted 並呼叫之後執行。 若要變更預設行為,請在呼叫 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 中呼叫的任何方法應立即傳回。

不過,要求取消之後不會放棄工作-呼叫端會等候所有工作完成。

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

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

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

BackgroundService 基類

BackgroundService 是實作長時間執行的 IHostedService 基類。

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

呼叫 IHostedService.StopAsync 時,就會觸發取消權杖。 當引發取消權杖以正常關閉服務時,您的 實 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 完成,因此此處說明的方法可能不適用於每個狀況。 Interlocked.Increment 可用來將執行計數器遞增為不可部分完成的作業,以確保多個執行緒不會同時更新 executionCount

服務會在 (中使用 Program.csAddHostedService 擴充方法) 註冊 IHostBuilder.ConfigureServices

services.AddHostedService<TimedHostedService>();

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

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

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

  • 服務為非同步。 DoWork 方法會傳回 Task。 基於示範用途,在 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 會傳回在 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);
    }
}

服務會在 (Program.cs) 中 IHostBuilder.ConfigureServices 註冊。 託管服務會向 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);
    }
}

每次在輸入裝置上選取 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("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);
        }
    }
}

服務會在 (Program.cs) 中 IHostBuilder.ConfigureServices 註冊。 託管服務會向 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();

其他資源