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

作者:Jeow Li Huan

注意

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

重要

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

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

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

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

背景工作服務範本

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

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

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

這些服務會在 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);

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

這些服務會在 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);
});

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

背景工作角色服務範本支援具有 --aot 旗標的 .NET 原生預先 (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 介面。 本文提供三個託管服務範例:

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

背景工作服務範本

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

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

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

這些服務會在 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);

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

這些服務會在 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);
});

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 介面。 本文提供三個託管服務範例:

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

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

背景工作服務範本

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

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

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

這些服務會在 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);

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

這些服務會在 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();

其他資源