在 ASP.NET Core 中使用託管服務的背景工作
作者:Jeow Li Huan
注意
這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本。
警告
不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支援原則。 如需目前版本,請參閱本文的 .NET 8 版本。
在 ASP.NET Core 中,背景工作可實作為「託管服務」。 託管服務是具有背景工作邏輯的類別,可實作 IHostedService 介面。 本文提供三個託管服務範例:
- 在計時器上執行的背景工作。
- 啟動具範圍服務的託管服務。 範圍服務可以使用相依性插入 (DI)。
- 以循序方式執行的排入佇列背景工作。
背景工作服務範本
ASP.NET Core 背景工作服務範本提供撰寫長期執行服務應用程式的起點。 從背景工作角色服務範本建立的應用程式會在其專案檔中指定背景工作角色 SDK:
<Project Sdk="Microsoft.NET.Sdk.Worker">
使用範本作為裝載服務應用程式的基礎:
- 建立新專案。
- 選取 [背景工作服務]。 選取 [下一步] 。
- 在 [專案名稱] 欄位中提供專案名稱,或接受預設專案名稱。 選取 [下一步] 。
- 在 [其他資訊] 對話方塊中,選擇 [架構]。 選取 建立。
套件
以背景工作角色服務範本為基礎的應用程式會使用 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
:
- 應用程式的要求處理管線已設定。
- 伺服器已啟動,並觸發 IApplicationLifetime.ApplicationStarted。
StartAsync
應該受限於短期執行的工作,因為託管服務會循序執行,且在 StartAsync
執行到完成之前,不會再啟動任何進一步的服務。
StopAsync
- 當主機執行正常關機程序時會觸發 StopAsync(CancellationToken)。
StopAsync
包含用來結束背景工作的邏輯。 實作 IDisposable 和 完成項 (解構函式) 以處置任何非受控的資源。
取消權杖有 30 秒的逾時預設值,以表示關機程序應該不再順利。 在權杖上要求取消時:
- 應終止應用程式正在執行的任何剩餘背景作業。
- 在
StopAsync
中呼叫的任何方法應立即傳回。
不過,不會在要求取消後直接放棄工作 — 呼叫者會等待所有工作完成。
如果應用程式意外關閉 (例如,應用程式的處理序失敗),可能不會呼叫 StopAsync
。 因此,任何在 StopAsync
中所呼叫方法或所執行作業可能不會發生。
若要延長預設的 30 秒鐘關機逾時,請設定:
- ShutdownTimeout (使用泛型主機時)。 如需詳細資訊,請參閱 ASP.NET 中的 .NET 泛型主機。
- 使用 Web 主機時,關機逾時會裝載組態設定。 如需詳細資訊,請參閱 ASP.NET Core Web 主機。
託管服務會在應用程式啟動時隨即啟動,然後在應用程式關閉時正常關閉。 如果在背景工作執行期間擲回錯誤,即使未呼叫 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
以將工作項目加入佇列。 - 工作項目會模擬長時間執行的背景工作:
- 執行三次 5 秒的延遲 (
Task.Delay
)。 - 如果取消工作,
try-catch
陳述式會截獲 OperationCanceledException。
- 執行三次 5 秒的延遲 (
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):
- 建立新專案。
- 選取 [背景工作服務]。 選取 [下一步] 。
- 在 [專案名稱] 欄位中提供專案名稱,或接受預設專案名稱。 選取 [下一步] 。
- 在 [其他資訊] 對話方塊中:
- 選擇架構。
- 核取 [啟用原生 AOT 發佈] 核取方塊。
- 選取建立。
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">
使用範本作為裝載服務應用程式的基礎:
- 建立新專案。
- 選取 [背景工作服務]。 選取 [下一步] 。
- 在 [專案名稱] 欄位中提供專案名稱,或接受預設專案名稱。 選取 [下一步] 。
- 在 [其他資訊] 對話方塊中,選擇 [架構]。 選取 建立。
套件
以背景工作角色服務範本為基礎的應用程式會使用 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
:
- 應用程式的要求處理管線已設定。
- 伺服器已啟動,並觸發 IApplicationLifetime.ApplicationStarted。
StartAsync
應該受限於短期執行的工作,因為託管服務會循序執行,且在 StartAsync
執行到完成之前,不會再啟動任何進一步的服務。
StopAsync
- 當主機執行正常關機程序時會觸發 StopAsync(CancellationToken)。
StopAsync
包含用來結束背景工作的邏輯。 實作 IDisposable 和 完成項 (解構函式) 以處置任何非受控的資源。
取消權杖有 30 秒的逾時預設值,以表示關機程序應該不再順利。 在權杖上要求取消時:
- 應終止應用程式正在執行的任何剩餘背景作業。
- 在
StopAsync
中呼叫的任何方法應立即傳回。
不過,不會在要求取消後直接放棄工作 — 呼叫者會等待所有工作完成。
如果應用程式意外關閉 (例如,應用程式的處理序失敗),可能不會呼叫 StopAsync
。 因此,任何在 StopAsync
中所呼叫方法或所執行作業可能不會發生。
若要延長預設的 30 秒鐘關機逾時,請設定:
- ShutdownTimeout (使用泛型主機時)。 如需詳細資訊,請參閱 ASP.NET 中的 .NET 泛型主機。
- 使用 Web 主機時,關機逾時會裝載組態設定。 如需詳細資訊,請參閱 ASP.NET Core Web 主機。
託管服務會在應用程式啟動時隨即啟動,然後在應用程式關閉時正常關閉。 如果在背景工作執行期間擲回錯誤,即使未呼叫 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
以將工作項目加入佇列。 - 工作項目會模擬長時間執行的背景工作:
- 執行三次 5 秒的延遲 (
Task.Delay
)。 - 如果取消工作,
try-catch
陳述式會截獲 OperationCanceledException。
- 執行三次 5 秒的延遲 (
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">
使用範本作為裝載服務應用程式的基礎:
- 建立新專案。
- 選取 [背景工作服務]。 選取 [下一步] 。
- 在 [專案名稱] 欄位中提供專案名稱,或接受預設專案名稱。 選取建立。
- 在 [建立新的背景工作服務] 對話方塊中,選取 [建立]。
套件
以背景工作角色服務範本為基礎的應用程式會使用 Microsoft.NET.Sdk.Worker
SDK,且具有 Microsoft.Extensions.Hosting 套件的明確套件參考。 例如,請參閱範例應用程式的專案檔 (BackgroundTasksSample.csproj
)。
對於使用 Microsoft.NET.Sdk.Web
SDK 的 Web 應用程式,Microsoft.Extensions.Hosting 套件會從共用架構隱含參考。 不需要應用程式專案檔中的明確套件參考。
IHostedService 介面
IHostedService 介面針對主機所管理的物件定義兩種方法:
StartAsync
StartAsync
包含用來啟動背景工作的邏輯。 在之前呼叫 StartAsync
:
- 應用程式的要求處理管線已設定。
- 伺服器已啟動,並觸發 IApplicationLifetime.ApplicationStarted。
您可以變更預設行為,讓託管服務的 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(CancellationToken)。
StopAsync
包含用來結束背景工作的邏輯。 實作 IDisposable 和 完成項 (解構函式) 以處置任何非受控的資源。
取消權杖有五秒的逾時預設值,以表示關機程序應該不再順利。 在權杖上要求取消時:
- 應終止應用程式正在執行的任何剩餘背景作業。
- 在
StopAsync
中呼叫的任何方法應立即傳回。
不過,不會在要求取消後直接放棄工作 — 呼叫者會等待所有工作完成。
如果應用程式意外關閉 (例如,應用程式的處理序失敗),可能不會呼叫 StopAsync
。 因此,任何在 StopAsync
中所呼叫方法或所執行作業可能不會發生。
若要延長預設的五秒鐘關機逾時,請設定:
- ShutdownTimeout (使用泛型主機時)。 如需詳細資訊,請參閱 ASP.NET 中的 .NET 泛型主機。
- 使用 Web 主機時,關機逾時會裝載組態設定。 如需詳細資訊,請參閱 ASP.NET Core Web 主機。
託管服務會在應用程式啟動時隨即啟動,然後在應用程式關閉時正常關閉。 如果在背景工作執行期間擲回錯誤,即使未呼叫 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
以將工作項目加入佇列。 - 工作項目會模擬長時間執行的背景工作:
- 執行三次 5 秒的延遲 (
Task.Delay
)。 - 如果取消工作,
try-catch
陳述式會截獲 OperationCanceledException。
- 執行三次 5 秒的延遲 (
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();