Hintergrundtasks mit gehosteten Diensten in ASP.NET Core
Von Jeow Li Huan
Hinweis
Dies ist nicht die neueste Version dieses Artikels. Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.
Warnung
Diese Version von ASP.NET Core wird nicht mehr unterstützt. Weitere Informationen finden Sie in der .NET- und .NET Core-Supportrichtlinie. Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.
Wichtig
Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Freigabe möglicherweise noch wesentlichen Änderungen unterliegt. Microsoft gibt keine Garantie, weder ausdrücklich noch impliziert, hinsichtlich der hier bereitgestellten Informationen.
Die aktuelle Version finden Sie in der .NET 9-Version dieses Artikels.
In ASP.NET Core können Hintergrundtasks als gehostete Dienste implementiert werden. Ein gehosteter Dienst ist eine Klasse mit Logik für Hintergrundaufgaben, die die Schnittstelle IHostedService implementiert. In diesem Artikel sind drei Beispiel für gehostete Dienste enthalten:
- Hintergrundtasks, die auf einem Timer ausgeführt werden.
- Gehostete Dienste, die einen bereichsbezogenen Dienst aktivieren. Der bereichsbezogene Dienst kann eine Abhängigkeitsinjektion (Dependency Injection, DI) verwenden.
- Hintergrundtasks in der Warteschlange, die sequenziell ausgeführt werden.
Workerdienstvorlage
Die ASP.NET Core-Vorlage „Workerdienst“ dient als Ausgangspunkt für das Schreiben von Dienstanwendungen mit langer Laufzeit. Eine aus der Workerdienstvorlage erstellte App gibt das Worker SDK in ihrer Projektdatei an:
<Project Sdk="Microsoft.NET.Sdk.Worker">
Gehen Sie folgendermaßen vor, wenn Sie die Vorlage als Grundlage für eine Hosted Services-App verwenden möchten:
- Erstellen Sie ein neues Projekt.
- Wählen Sie Workerdienst aus. Klicken Sie auf Weiter.
- Geben Sie im Feld Projektname einen Projektnamen ein, oder übernehmen Sie den Standardnamen. Wählen Sie Weiter aus.
- Wählen Sie im Dialogfeld Zusätzliche Informationen ein Framework aus. Klicken Sie auf Erstellen.
Paket
Eine App, die auf der Workerdienstvorlage basiert, verwendet das Microsoft.NET.Sdk.Worker
SDK und verfügt über einen expliziten Paketverweis auf das Microsoft.Extensions.Hosting-Paket. Sehen Sie sich dazu beispielsweise die Projektdatei der Beispiel-App (BackgroundTasksSample.csproj
) an.
Für Web-Apps, die das Microsoft.NET.Sdk.Web
SDK verwenden, wird auf das Microsoft.Extensions.Hosting-Paket implizit über das geteilte Framework verwiesen. Es ist kein expliziter Paketverweis in der Projektdatei der App erforderlich.
Die IHostedService-Schnittstelle
Die IHostedService-Schnittstelle definiert zwei Methoden für Objekte, die vom Host verwaltet werden:
StartAsync
StartAsync(CancellationToken) enthält die Logik zum Starten der Hintergrundaufgabe. StartAsync
wird vor folgenden Vorgängen aufgerufen:
- Die App-Pipeline für die Anforderungsverarbeitung wird konfiguriert.
- Der Server wird gestartet, und IApplicationLifetime.ApplicationStarted wird ausgelöst.
StartAsync
sollte auf Aufgaben mit kurzer Ausführung beschränkt werden, da gehostete Dienste sequenziell ausgeführt werden, sodass weitere Dienste erst gestartet werden, wenn die Ausführung von StartAsync
beendet wurde.
StopAsync
- StopAsync(CancellationToken) wird ausgelöst, wenn der Host das Herunterfahren ordnungsgemäß ausführt.
StopAsync
enthält die Logik zum Beenden des Hintergrundtasks. Implementieren Sie IDisposable und Finalizer (Destruktoren), um nicht verwaltete Ressourcen zu löschen.
Das Abbruchtoken hat standardmäßig ein Zeitlimit von 30 Sekunden, um zu melden, dass der Prozess des Herunterfahrens nicht mehr ordnungsgemäß ausgeführt wird. Gehen Sie wie folgt vor, wenn ein Abbruch für das Token angefordert wird:
- Brechen Sie jegliche von der App ausgeführten Hintergrundoperationen ab.
- Jegliche Methoden, die in
StopAsync
aufgerufen werden, sollten umgehend zurückgegeben werden.
Allerdings werden keine Aufgaben abgebrochen, wenn der Abbruch angefordert wird. Der Aufrufer wartet, bis alle Aufgaben abgeschlossen sind.
Wenn die App unerwartet beendet wird (weil der Prozess der App beispielsweise fehlschlägt), wird StopAsync
möglicherweise nicht aufgerufen. Daher werden die in StopAsync
aufgerufenen Methoden oder ausgeführten Operationen nicht durchgeführt.
Um das standardmäßig 30-sekündige Timeout beim Herunterfahren zu verlängern, legen Sie Folgendes fest:
- ShutdownTimeout, wenn Sie den generischen Host verwenden. Weitere Informationen finden Sie unter Generischer .NET-Host in ASP.NET Core.
- Hostkonfigurationseinstellung „Timeout beim Herunterfahren“, wenn Sie den Webhost verwenden. Weitere Informationen finden Sie unter ASP.NET Core-Webhost.
Der gehostete Dienst wird beim Start der App einmal aktiviert und beim Beenden der App wieder ordnungsgemäß heruntergefahren. Wenn während der Ausführung von Hintergrundtasks ein Fehler ausgelöst wird, sollte Dispose
aufgerufen werden, auch wenn StopAsync
nicht aufgerufen wird.
BackgroundService-Basisklasse
BackgroundService ist eine Basisklasse zur Implementierung eines IHostedService mit langer Laufzeit.
ExecuteAsync (CancellationToken) wird aufgerufen, um den Hintergrunddienst auszuführen. Die Implementierung gibt einen Task zurück, der die gesamte Lebensdauer des Hintergrunddiensts darstellt. Es werden keine weiteren Dienste gestartet, bis ExecuteAsync asynchron wird, etwa durch den Aufruf von await
. Vermeiden Sie die Ausführung von langen, blockierenden Initialisierungsarbeiten in ExecuteAsync
. Die Hostblöcke in StopAsync(CancellationToken) warten auf den Abschluss von ExecuteAsync
.
Das Abbruchtoken wird beim Aufruf von IHostedService.StopAsync ausgelöst. Ihre Implementierung von ExecuteAsync
sollte unverzüglich beendet werden, wenn das Abbruchtoken ausgelöst wird, um den Dienst ordnungsgemäß herunterzufahren. Andernfalls wird der Dienst beim Erreichen des Timeouts beim Herunterfahren nicht ordnungsgemäß beendet. Weitere Informationen finden Sie im Abschnitt IHostedService-Schnittstelle.
Weitere Informationen finden Sie im Quellcode für BackgroundService.
Zeitlich festgelegte Hintergrundtasks
Zeitlich festgelegte Hintergrundtasks verwenden die Klasse System.Threading.Timer. Der Timer löst die DoWork
-Methode des Tasks aus. Der Timer wird durch StopAsync
deaktiviert und freigegeben, wenn der Dienstcontainer durch Dispose
freigegeben ist:
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 wartet nicht auf den Abschluss vorheriger Ausführungen von DoWork
. Die veranschaulichte Vorgehensweise eignet sich also möglicherweise nicht für alle Szenarios. Interlocked.Increment wird zum Erhöhen des Ausführungszählers mit einem atomischen Vorgang verwendet, wodurch sichergestellt wird, dass executionCount
nicht durch mehrere Threads gleichzeitig aktualisiert wird.
Der Dienst wird in IHostBuilder.ConfigureServices
(Program.cs
) mit der Erweiterungsmethode AddHostedService
registriert:
services.AddHostedService<TimedHostedService>();
Verwenden eines bereichsbezogenen Diensts in einem Hintergrundtask
Erstellen Sie einen Bereich, um bereichsbezogene Dienste in einem BackgroundService zu verwenden. Bereiche werden für einen gehosteten Dienst nicht standardmäßig erstellt.
Der bereichsbezogene Dienst für Hintergrundtasks enthält die Logik des Hintergrundtasks. Im folgenden Beispiel:
- Der Dienst ist asynchron. Die
DoWork
-Methode gibtTask
zurück. Zu Demonstrationszwecken wird in derDoWork
-Methode eine Verzögerung von zehn Sekunden verwendet. - Ein ILogger wird in den Dienst eingefügt.
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);
}
}
}
Der gehostete Dienst erstellt einen Bereich, um den bereichsbezogenen Dienst für Hintergrundtasks aufzulösen, damit die DoWork
-Methode aufgerufen wird. DoWork
gibt einen Task
zurück, auf den in ExecuteAsync
gewartet wird:
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);
}
}
Die Dienste werden in IHostBuilder.ConfigureServices
(Program.cs
) registriert. Der gehostete Dienst wird mit der Erweiterungsmethode AddHostedService
registriert:
services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
Hintergrundtasks in der Warteschlange
Eine Warteschlange für Hintergrundaufgaben basiert auf dem .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;
}
}
Im folgenden Beispiel für QueueHostedService
gilt:
- Die
BackgroundProcessing
-Methode gibt einenTask
zurück, auf den inExecuteAsync
gewartet wird. - Hintergrundtasks in der Warteschlange werden aus dieser entfernt und in
BackgroundProcessing
ausgeführt. - Auf Arbeitselemente wird gewartet, bevor der Dienst in
StopAsync
angehalten wird.
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);
}
}
Ein MonitorLoop
-Dienst verarbeitet das Einreihen von Tasks in die Warteschlange für den gehosteten Dienst, wenn der w
-Schlüssel auf einem Eingabegerät ausgewählt wird:
- Die
IBackgroundTaskQueue
wird in denMonitorLoop
-Dienst eingefügt. IBackgroundTaskQueue.QueueBackgroundWorkItem
wird aufgerufen, um ein Arbeitselement in die Warteschlange einzureihen.- Das Arbeitselement simuliert eine Hintergrundaufgabe mit langer Ausführungszeit:
- Drei 5-Sekunden-Verzögerungen werden ausgeführt (
Task.Delay
). - Eine
try-catch
-Anweisung fängt OperationCanceledException auf, wenn der Task abgebrochen wird.
- Drei 5-Sekunden-Verzögerungen werden ausgeführt (
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);
}
}
}
Die Dienste werden in IHostBuilder.ConfigureServices
(Program.cs
) registriert. Der gehostete Dienst wird mit der Erweiterungsmethode AddHostedService
registriert:
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
wird in Program.cs
gestartet:
var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();
Asynchron terminierte Hintergrundaufgabe
Mit dem folgenden Code wird eine asynchron terminierte Hintergrundaufgabe erstellt:
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);
}
}
Natives AOT
Die Workerdienstvorlagen unterstützen die native AOT-Option (ahead-of-time) von .NET mit dem --aot
-Flag:
- Erstellen Sie ein neues Projekt.
- Wählen Sie Workerdienst aus. Klicken Sie auf Weiter.
- Geben Sie im Feld Projektname einen Projektnamen ein, oder übernehmen Sie den Standardnamen. Wählen Sie Weiter aus.
- Im Dialogfeld Zusätzliche Informationen:
- Wählen Sie ein Framework aus.
- Klicken Sie das Kontrollkästchen Native AOT-Veröffentlichung aktivieren.
- Klicken Sie auf Erstellen.
Die AOT-Option fügt der Projektdatei <PublishAot>true</PublishAot>
hinzu:
<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>
Zusätzliche Ressourcen
- Komponententests für Hintergrunddienste auf GitHub.
- Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)
- Implement background tasks in microservices with IHostedService and the BackgroundService class (Implementieren von Hintergrundtasks in Microservices mit IHostedService und der BackgroundService-Klasse)
- Ausführen von Hintergrundaufgaben mit WebJobs in Azure App Service
- Timer
In ASP.NET Core können Hintergrundtasks als gehostete Dienste implementiert werden. Ein gehosteter Dienst ist eine Klasse mit Logik für Hintergrundaufgaben, die die Schnittstelle IHostedService implementiert. In diesem Artikel sind drei Beispiel für gehostete Dienste enthalten:
- Hintergrundtasks, die auf einem Timer ausgeführt werden.
- Gehostete Dienste, die einen bereichsbezogenen Dienst aktivieren. Der bereichsbezogene Dienst kann eine Abhängigkeitsinjektion (Dependency Injection, DI) verwenden.
- Hintergrundtasks in der Warteschlange, die sequenziell ausgeführt werden.
Workerdienstvorlage
Die ASP.NET Core-Vorlage „Workerdienst“ dient als Ausgangspunkt für das Schreiben von Dienstanwendungen mit langer Laufzeit. Eine aus der Workerdienstvorlage erstellte App gibt das Worker SDK in ihrer Projektdatei an:
<Project Sdk="Microsoft.NET.Sdk.Worker">
Gehen Sie folgendermaßen vor, wenn Sie die Vorlage als Grundlage für eine Hosted Services-App verwenden möchten:
- Erstellen Sie ein neues Projekt.
- Wählen Sie Workerdienst aus. Klicken Sie auf Weiter.
- Geben Sie im Feld Projektname einen Projektnamen ein, oder übernehmen Sie den Standardnamen. Wählen Sie Weiter aus.
- Wählen Sie im Dialogfeld Zusätzliche Informationen ein Framework aus. Klicken Sie auf Erstellen.
Paket
Eine App, die auf der Workerdienstvorlage basiert, verwendet das Microsoft.NET.Sdk.Worker
SDK und verfügt über einen expliziten Paketverweis auf das Microsoft.Extensions.Hosting-Paket. Sehen Sie sich dazu beispielsweise die Projektdatei der Beispiel-App (BackgroundTasksSample.csproj
) an.
Für Web-Apps, die das Microsoft.NET.Sdk.Web
SDK verwenden, wird auf das Microsoft.Extensions.Hosting-Paket implizit über das geteilte Framework verwiesen. Es ist kein expliziter Paketverweis in der Projektdatei der App erforderlich.
Die IHostedService-Schnittstelle
Die IHostedService-Schnittstelle definiert zwei Methoden für Objekte, die vom Host verwaltet werden:
StartAsync
StartAsync(CancellationToken) enthält die Logik zum Starten der Hintergrundaufgabe. StartAsync
wird vor folgenden Vorgängen aufgerufen:
- Die App-Pipeline für die Anforderungsverarbeitung wird konfiguriert.
- Der Server wird gestartet, und IApplicationLifetime.ApplicationStarted wird ausgelöst.
StartAsync
sollte auf Aufgaben mit kurzer Ausführung beschränkt werden, da gehostete Dienste sequenziell ausgeführt werden, sodass weitere Dienste erst gestartet werden, wenn die Ausführung von StartAsync
beendet wurde.
StopAsync
- StopAsync(CancellationToken) wird ausgelöst, wenn der Host das Herunterfahren ordnungsgemäß ausführt.
StopAsync
enthält die Logik zum Beenden des Hintergrundtasks. Implementieren Sie IDisposable und Finalizer (Destruktoren), um nicht verwaltete Ressourcen zu löschen.
Das Abbruchtoken hat standardmäßig ein Zeitlimit von 30 Sekunden, um zu melden, dass der Prozess des Herunterfahrens nicht mehr ordnungsgemäß ausgeführt wird. Gehen Sie wie folgt vor, wenn ein Abbruch für das Token angefordert wird:
- Brechen Sie jegliche von der App ausgeführten Hintergrundoperationen ab.
- Jegliche Methoden, die in
StopAsync
aufgerufen werden, sollten umgehend zurückgegeben werden.
Allerdings werden keine Aufgaben abgebrochen, wenn der Abbruch angefordert wird. Der Aufrufer wartet, bis alle Aufgaben abgeschlossen sind.
Wenn die App unerwartet beendet wird (weil der Prozess der App beispielsweise fehlschlägt), wird StopAsync
möglicherweise nicht aufgerufen. Daher werden die in StopAsync
aufgerufenen Methoden oder ausgeführten Operationen nicht durchgeführt.
Um das standardmäßig 30-sekündige Timeout beim Herunterfahren zu verlängern, legen Sie Folgendes fest:
- ShutdownTimeout, wenn Sie den generischen Host verwenden. Weitere Informationen finden Sie unter Generischer .NET-Host in ASP.NET Core.
- Hostkonfigurationseinstellung „Timeout beim Herunterfahren“, wenn Sie den Webhost verwenden. Weitere Informationen finden Sie unter ASP.NET Core-Webhost.
Der gehostete Dienst wird beim Start der App einmal aktiviert und beim Beenden der App wieder ordnungsgemäß heruntergefahren. Wenn während der Ausführung von Hintergrundtasks ein Fehler ausgelöst wird, sollte Dispose
aufgerufen werden, auch wenn StopAsync
nicht aufgerufen wird.
BackgroundService-Basisklasse
BackgroundService ist eine Basisklasse zur Implementierung eines IHostedService mit langer Laufzeit.
ExecuteAsync (CancellationToken) wird aufgerufen, um den Hintergrunddienst auszuführen. Die Implementierung gibt einen Task zurück, der die gesamte Lebensdauer des Hintergrunddiensts darstellt. Es werden keine weiteren Dienste gestartet, bis ExecuteAsync asynchron wird, etwa durch den Aufruf von await
. Vermeiden Sie die Ausführung von langen, blockierenden Initialisierungsarbeiten in ExecuteAsync
. Die Hostblöcke in StopAsync(CancellationToken) warten auf den Abschluss von ExecuteAsync
.
Das Abbruchtoken wird beim Aufruf von IHostedService.StopAsync ausgelöst. Ihre Implementierung von ExecuteAsync
sollte unverzüglich beendet werden, wenn das Abbruchtoken ausgelöst wird, um den Dienst ordnungsgemäß herunterzufahren. Andernfalls wird der Dienst beim Erreichen des Timeouts beim Herunterfahren nicht ordnungsgemäß beendet. Weitere Informationen finden Sie im Abschnitt IHostedService-Schnittstelle.
Weitere Informationen finden Sie im Quellcode für BackgroundService.
Zeitlich festgelegte Hintergrundtasks
Zeitlich festgelegte Hintergrundtasks verwenden die Klasse System.Threading.Timer. Der Timer löst die DoWork
-Methode des Tasks aus. Der Timer wird durch StopAsync
deaktiviert und freigegeben, wenn der Dienstcontainer durch Dispose
freigegeben ist:
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 wartet nicht auf den Abschluss vorheriger Ausführungen von DoWork
. Die veranschaulichte Vorgehensweise eignet sich also möglicherweise nicht für alle Szenarios. Interlocked.Increment wird zum Erhöhen des Ausführungszählers mit einem atomischen Vorgang verwendet, wodurch sichergestellt wird, dass executionCount
nicht durch mehrere Threads gleichzeitig aktualisiert wird.
Der Dienst wird in IHostBuilder.ConfigureServices
(Program.cs
) mit der Erweiterungsmethode AddHostedService
registriert:
services.AddHostedService<TimedHostedService>();
Verwenden eines bereichsbezogenen Diensts in einem Hintergrundtask
Erstellen Sie einen Bereich, um bereichsbezogene Dienste in einem BackgroundService zu verwenden. Bereiche werden für einen gehosteten Dienst nicht standardmäßig erstellt.
Der bereichsbezogene Dienst für Hintergrundtasks enthält die Logik des Hintergrundtasks. Im folgenden Beispiel:
- Der Dienst ist asynchron. Die
DoWork
-Methode gibtTask
zurück. Zu Demonstrationszwecken wird in derDoWork
-Methode eine Verzögerung von zehn Sekunden verwendet. - Ein ILogger wird in den Dienst eingefügt.
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);
}
}
}
Der gehostete Dienst erstellt einen Bereich, um den bereichsbezogenen Dienst für Hintergrundtasks aufzulösen, damit die DoWork
-Methode aufgerufen wird. DoWork
gibt einen Task
zurück, auf den in ExecuteAsync
gewartet wird:
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);
}
}
Die Dienste werden in IHostBuilder.ConfigureServices
(Program.cs
) registriert. Der gehostete Dienst wird mit der Erweiterungsmethode AddHostedService
registriert:
services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
Hintergrundtasks in der Warteschlange
Eine Warteschlange für Hintergrundaufgaben basiert auf dem .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;
}
}
Im folgenden Beispiel für QueueHostedService
gilt:
- Die
BackgroundProcessing
-Methode gibt einenTask
zurück, auf den inExecuteAsync
gewartet wird. - Hintergrundtasks in der Warteschlange werden aus dieser entfernt und in
BackgroundProcessing
ausgeführt. - Auf Arbeitselemente wird gewartet, bevor der Dienst in
StopAsync
angehalten wird.
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);
}
}
Ein MonitorLoop
-Dienst verarbeitet das Einreihen von Tasks in die Warteschlange für den gehosteten Dienst, wenn der w
-Schlüssel auf einem Eingabegerät ausgewählt wird:
- Die
IBackgroundTaskQueue
wird in denMonitorLoop
-Dienst eingefügt. IBackgroundTaskQueue.QueueBackgroundWorkItem
wird aufgerufen, um ein Arbeitselement in die Warteschlange einzureihen.- Das Arbeitselement simuliert eine Hintergrundaufgabe mit langer Ausführungszeit:
- Drei 5-Sekunden-Verzögerungen werden ausgeführt (
Task.Delay
). - Eine
try-catch
-Anweisung fängt OperationCanceledException auf, wenn der Task abgebrochen wird.
- Drei 5-Sekunden-Verzögerungen werden ausgeführt (
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);
}
}
}
Die Dienste werden in IHostBuilder.ConfigureServices
(Program.cs
) registriert. Der gehostete Dienst wird mit der Erweiterungsmethode AddHostedService
registriert:
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
wird in Program.cs
gestartet:
var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();
Asynchron terminierte Hintergrundaufgabe
Mit dem folgenden Code wird eine asynchron terminierte Hintergrundaufgabe erstellt:
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);
}
}
Zusätzliche Ressourcen
- Komponententests für Hintergrunddienste auf GitHub.
- Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)
- Implement background tasks in microservices with IHostedService and the BackgroundService class (Implementieren von Hintergrundtasks in Microservices mit IHostedService und der BackgroundService-Klasse)
- Ausführen von Hintergrundaufgaben mit WebJobs in Azure App Service
- Timer
In ASP.NET Core können Hintergrundtasks als gehostete Dienste implementiert werden. Ein gehosteter Dienst ist eine Klasse mit Logik für Hintergrundaufgaben, die die Schnittstelle IHostedService implementiert. In diesem Artikel sind drei Beispiel für gehostete Dienste enthalten:
- Hintergrundtasks, die auf einem Timer ausgeführt werden.
- Gehostete Dienste, die einen bereichsbezogenen Dienst aktivieren. Der bereichsbezogene Dienst kann eine Abhängigkeitsinjektion (Dependency Injection, DI) verwenden.
- Hintergrundtasks in der Warteschlange, die sequenziell ausgeführt werden.
Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)
Vorlage „Workerdienst“
Die ASP.NET Core-Vorlage „Workerdienst“ dient als Ausgangspunkt für das Schreiben von Dienstanwendungen mit langer Laufzeit. Eine aus der Workerdienstvorlage erstellte App gibt das Worker SDK in ihrer Projektdatei an:
<Project Sdk="Microsoft.NET.Sdk.Worker">
Gehen Sie folgendermaßen vor, wenn Sie die Vorlage als Grundlage für eine Hosted Services-App verwenden möchten:
- Erstellen Sie ein neues Projekt.
- Wählen Sie Workerdienst aus. Klicken Sie auf Weiter.
- Geben Sie im Feld Projektname einen Projektnamen ein, oder übernehmen Sie den Standardnamen. Klicken Sie auf Erstellen.
- Wählen Sie im Dialogfeld Neuen Workerdienst erstellenErstellen aus.
Paket
Eine App, die auf der Workerdienstvorlage basiert, verwendet das Microsoft.NET.Sdk.Worker
SDK und verfügt über einen expliziten Paketverweis auf das Microsoft.Extensions.Hosting-Paket. Sehen Sie sich dazu beispielsweise die Projektdatei der Beispiel-App (BackgroundTasksSample.csproj
) an.
Für Web-Apps, die das Microsoft.NET.Sdk.Web
SDK verwenden, wird auf das Microsoft.Extensions.Hosting-Paket implizit über das geteilte Framework verwiesen. Es ist kein expliziter Paketverweis in der Projektdatei der App erforderlich.
Die IHostedService-Schnittstelle
Die IHostedService-Schnittstelle definiert zwei Methoden für Objekte, die vom Host verwaltet werden:
StartAsync
StartAsync
enthält die Logik zum Starten des Hintergrundtasks. StartAsync
wird vor folgenden Vorgängen aufgerufen:
- Die App-Pipeline für die Anforderungsverarbeitung wird konfiguriert.
- Der Server wird gestartet, und IApplicationLifetime.ApplicationStarted wird ausgelöst.
Das Standardverhalten kann so geändert werden, dass der StartAsync
-Vorgang des gehosteten Diensts ausgeführt wird, nachdem die Pipeline der App konfiguriert und ApplicationStarted
aufgerufen wurde. Um das Standardverhalten zu ändern, fügen Sie den gehosteten Dienst (VideosWatcher
im folgenden Beispiel) nach dem Aufruf von ConfigureWebHostDefaults
hinzu:
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) wird ausgelöst, wenn der Host das Herunterfahren ordnungsgemäß ausführt.
StopAsync
enthält die Logik zum Beenden des Hintergrundtasks. Implementieren Sie IDisposable und Finalizer (Destruktoren), um nicht verwaltete Ressourcen zu löschen.
Das Abbruchtoken hat standardmäßig ein Zeitlimit von fünf Sekunden, um zu melden, dass der Prozess des Herunterfahrens nicht mehr ordnungsgemäß ausgeführt wird. Gehen Sie wie folgt vor, wenn ein Abbruch für das Token angefordert wird:
- Brechen Sie jegliche von der App ausgeführten Hintergrundoperationen ab.
- Jegliche Methoden, die in
StopAsync
aufgerufen werden, sollten umgehend zurückgegeben werden.
Allerdings werden keine Aufgaben abgebrochen, wenn der Abbruch angefordert wird. Der Aufrufer wartet, bis alle Aufgaben abgeschlossen sind.
Wenn die App unerwartet beendet wird (weil der Prozess der App beispielsweise fehlschlägt), wird StopAsync
möglicherweise nicht aufgerufen. Daher werden die in StopAsync
aufgerufenen Methoden oder ausgeführten Operationen nicht durchgeführt.
Um das standardmäßig 5-sekündige Timeout beim Herunterfahren zu verlängern, legen Sie folgendes fest:
- ShutdownTimeout, wenn Sie den generischen Host verwenden. Weitere Informationen finden Sie unter Generischer .NET-Host in ASP.NET Core.
- Hostkonfigurationseinstellung „Timeout beim Herunterfahren“, wenn Sie den Webhost verwenden. Weitere Informationen finden Sie unter ASP.NET Core-Webhost.
Der gehostete Dienst wird beim Start der App einmal aktiviert und beim Beenden der App wieder ordnungsgemäß heruntergefahren. Wenn während der Ausführung von Hintergrundtasks ein Fehler ausgelöst wird, sollte Dispose
aufgerufen werden, auch wenn StopAsync
nicht aufgerufen wird.
BackgroundService-Basisklasse
BackgroundService ist eine Basisklasse zur Implementierung eines IHostedService mit langer Laufzeit.
ExecuteAsync (CancellationToken) wird aufgerufen, um den Hintergrunddienst auszuführen. Die Implementierung gibt einen Task zurück, der die gesamte Lebensdauer des Hintergrunddiensts darstellt. Es werden keine weiteren Dienste gestartet, bis ExecuteAsync asynchron wird, etwa durch den Aufruf von await
. Vermeiden Sie die Ausführung von langen, blockierenden Initialisierungsarbeiten in ExecuteAsync
. Die Hostblöcke in StopAsync(CancellationToken) warten auf den Abschluss von ExecuteAsync
.
Das Abbruchtoken wird beim Aufruf von IHostedService.StopAsync ausgelöst. Ihre Implementierung von ExecuteAsync
sollte unverzüglich beendet werden, wenn das Abbruchtoken ausgelöst wird, um den Dienst ordnungsgemäß herunterzufahren. Andernfalls wird der Dienst beim Erreichen des Timeouts beim Herunterfahren nicht ordnungsgemäß beendet. Weitere Informationen finden Sie im Abschnitt IHostedService-Schnittstelle.
StartAsync
sollte auf Aufgaben mit kurzer Ausführung beschränkt werden, da gehostete Dienste sequenziell ausgeführt werden, sodass weitere Dienste erst gestartet werden, wenn die Ausführung von StartAsync
beendet wurde. Zeitintensive Aufgaben sollten in platziert ExecuteAsync
werden. Weitere Informationen finden Sie im Quellcode für BackgroundService.
Zeitlich festgelegte Hintergrundtasks
Zeitlich festgelegte Hintergrundtasks verwenden die Klasse System.Threading.Timer. Der Timer löst die DoWork
-Methode des Tasks aus. Der Timer wird durch StopAsync
deaktiviert und freigegeben, wenn der Dienstcontainer durch Dispose
freigegeben ist:
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 wartet nicht auf den Abschluss vorheriger Ausführungen von DoWork
. Die veranschaulichte Vorgehensweise eignet sich also möglicherweise nicht für alle Szenarios. Interlocked.Increment wird zum Erhöhen des Ausführungszählers mit einem atomischen Vorgang verwendet, wodurch sichergestellt wird, dass executionCount
nicht durch mehrere Threads gleichzeitig aktualisiert wird.
Der Dienst wird in IHostBuilder.ConfigureServices
(Program.cs
) mit der Erweiterungsmethode AddHostedService
registriert:
services.AddHostedService<TimedHostedService>();
Verwenden eines bereichsbezogenen Diensts in einem Hintergrundtask
Erstellen Sie einen Bereich, um bereichsbezogene Dienste in einem BackgroundService zu verwenden. Bereiche werden für einen gehosteten Dienst nicht standardmäßig erstellt.
Der bereichsbezogene Dienst für Hintergrundtasks enthält die Logik des Hintergrundtasks. Im folgenden Beispiel:
- Der Dienst ist asynchron. Die
DoWork
-Methode gibtTask
zurück. Zu Demonstrationszwecken wird in derDoWork
-Methode eine Verzögerung von zehn Sekunden verwendet. - Ein ILogger wird in den Dienst eingefügt.
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);
}
}
}
Der gehostete Dienst erstellt einen Bereich, um den bereichsbezogenen Dienst für Hintergrundtasks aufzulösen, damit die DoWork
-Methode aufgerufen wird. DoWork
gibt einen Task
zurück, auf den in ExecuteAsync
gewartet wird:
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);
}
}
Die Dienste werden in IHostBuilder.ConfigureServices
(Program.cs
) registriert. Der gehostete Dienst wird mit der Erweiterungsmethode AddHostedService
registriert:
services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
Hintergrundtasks in der Warteschlange
Eine Warteschlange für Hintergrundaufgaben basiert auf dem .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;
}
}
Im folgenden Beispiel für QueueHostedService
gilt:
- Die
BackgroundProcessing
-Methode gibt einenTask
zurück, auf den inExecuteAsync
gewartet wird. - Hintergrundtasks in der Warteschlange werden aus dieser entfernt und in
BackgroundProcessing
ausgeführt. - Auf Arbeitselemente wird gewartet, bevor der Dienst in
StopAsync
angehalten wird.
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);
}
}
Ein MonitorLoop
-Dienst verarbeitet das Einreihen von Tasks in die Warteschlange für den gehosteten Dienst, wenn der w
-Schlüssel auf einem Eingabegerät ausgewählt wird:
- Die
IBackgroundTaskQueue
wird in denMonitorLoop
-Dienst eingefügt. IBackgroundTaskQueue.QueueBackgroundWorkItem
wird aufgerufen, um ein Arbeitselement in die Warteschlange einzureihen.- Das Arbeitselement simuliert eine Hintergrundaufgabe mit langer Ausführungszeit:
- Drei 5-Sekunden-Verzögerungen werden ausgeführt (
Task.Delay
). - Eine
try-catch
-Anweisung fängt OperationCanceledException auf, wenn der Task abgebrochen wird.
- Drei 5-Sekunden-Verzögerungen werden ausgeführt (
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);
}
}
}
Die Dienste werden in IHostBuilder.ConfigureServices
(Program.cs
) registriert. Der gehostete Dienst wird mit der Erweiterungsmethode AddHostedService
registriert:
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
wird in Program.Main
gestartet:
var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();