Tareas en segundo plano con servicios hospedados en ASP.NET Core
Por Jeow Li Huan
Nota
Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión .NET 8 de este artículo.
Advertencia
Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulte la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulte la versión .NET 8 de este artículo.
Importante
Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.
Para la versión actual, consulte la versión .NET 8 de este artículo.
En ASP.NET Core, las tareas en segundo plano se pueden implementar como servicios hospedados. Un servicio hospedado es una clase con lógica de tarea en segundo plano que implementa la interfaz IHostedService. En este artículo se incluyen tres ejemplos de servicio hospedado:
- Una tarea en segundo plano que se ejecuta según un temporizador.
- Un servicio hospedado que activa un servicio con ámbito. El servicio con ámbito puede usar la inserción de dependencias (DI).
- Tareas en segundo plano en cola que se ejecutan en secuencia.
Plantilla Worker Service
La plantilla Worker Service de ASP.NET Core sirve de punto de partida para escribir aplicaciones de servicio de larga duración. Una aplicación creada a partir de la plantilla Worker Service especifica el SDK de trabajo en su archivo del proyecto:
<Project Sdk="Microsoft.NET.Sdk.Worker">
Para usar la plantilla como base de una aplicación de servicios hospedados:
- Cree un nuevo proyecto.
- Seleccione Worker Service (Servicio de Worker). Seleccione Siguiente.
- Proporcione un nombre para el proyecto en el campo Nombre del proyecto o acepte el predeterminado. Seleccione Siguiente.
- En el cuadro de diálogo Información adicional, elija Marco de trabajo. Seleccione Crear.
Paquete
Una aplicación basada en la plantilla Worker Service usa el SDK de Microsoft.NET.Sdk.Worker
y tiene una referencia de paquete explícita al paquete Microsoft.Extensions.Hosting. Por ejemplo, consulte el archivo del proyecto de la aplicación de ejemplo (BackgroundTasksSample.csproj
).
En el caso de las aplicaciones web que usan el SDK de Microsoft.NET.Sdk.Web
, desde el marco compartido se hace una referencia implícita al paquete Microsoft.Extensions.Hosting. No se requiere una referencia de paquete explícita en el archivo del proyecto de la aplicación.
Interfaz IHostedService
La interfaz IHostedService define dos métodos para los objetos administrados por el host:
StartAsync
StartAsync(CancellationToken) contiene la lógica para iniciar la tarea en segundo plano. Se llama a StartAsync
antes de que:
- La canalización de procesamiento de solicitudes de la aplicación está configurada.
- El servidor se haya iniciado y IApplicationLifetime.ApplicationStarted se haya activado.
StartAsync
debe limitarse a tareas de ejecución corta porque los servicios hospedados se ejecutan secuencialmente, y no se inician más servicios hasta que StartAsync
se ejecuta hasta su finalización.
StopAsync
- StopAsync(CancellationToken) se activa cuando el host está realizando un cierre estable.
StopAsync
contiene la lógica para finalizar la tarea en segundo plano. Implemente IDisposable y los finalizadores (destructores) para desechar los recursos no administrados.
El token de cancelación tiene un tiempo de espera predeterminado de 30 segundos para indicar que el proceso de cierre ya no debería ser estable. Cuando se solicita la cancelación en el token:
- Se deben anular las operaciones restantes en segundo plano que realiza la aplicación.
- Los métodos llamados en
StopAsync
deberían devolver contenido al momento.
Pero las tareas no se abandonan después de solicitar la cancelación, sino que el autor de la llamada espera a que se completen todas las tareas.
Si la aplicación se cierra inesperadamente (por ejemplo, porque se produzca un error en el proceso de la aplicación), puede que no sea posible llamar a StopAsync
. Por lo tanto, los métodos llamados o las operaciones llevadas a cabo en StopAsync
podrían no producirse.
Para ampliar el tiempo de espera predeterminado de apagado de 30 segundos, establezca:
- ShutdownTimeout cuando se usa el host genérico. Para obtener más información, consulte Hospedaje genérico de .NET en ASP.NET Core.
- Configuración de los valores de host de tiempo de espera de apagado cuando se usa el host web. Para obtener más información, consulte Host web de ASP.NET Core.
El servicio hospedado se activa una vez al inicio de la aplicación y se cierra de manera estable cuando dicha aplicación se cierra. Si se produce un error durante la ejecución de una tarea en segundo plano, hay que llamar a Dispose
, aun cuando no se haya llamado a StopAsync
.
Clase base BackgroundService
BackgroundService es una clase base para implementar un IHostedService de larga duración.
Se llama a ExecuteAsync(CancellationToken) para ejecutar el servicio en segundo plano. La implementación devuelve Task, que representa toda la duración del servicio en segundo plano. No se inicia ningún servicio hasta que ExecuteAsync se convierte en asincrónico, mediante una llamada a await
. Evite realizar un trabajo de inicialización de bloqueo prolongado en ExecuteAsync
. El host se bloquea en StopAsync(CancellationToken) a la espera de que ExecuteAsync
se complete.
El token de cancelación se desencadena cuando se llama a IHostedService.StopAsync. La implementación de ExecuteAsync
debe finalizar rápidamente cuando se active el token de cancelación para cerrar correctamente el servicio. De lo contrario, el servicio se cierra de manera abrupta durante el tiempo de expiración del cierre. Para más información, consulte la sección sobre la interfaz IHostedService.
Para obtener más información, vea el código fuente de BackgroundService.
Tareas en segundo plano temporizadas
Una tarea en segundo plano temporizada hace uso de la clase System.Threading.Timer. El temporizador activa el método DoWork
de la tarea. El temporizador está deshabilitado en StopAsync
y se desecha cuando el contenedor de servicios se elimina en 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 no espera a que finalicen las ejecuciones anteriores de DoWork
, por lo que es posible que el enfoque mostrado no sea adecuado para todos los escenarios. Interlocked.Increment se usa para incrementar el contador de ejecución como una operación atómica, lo que garantiza que varios subprocesos no actualicen executionCount
simultáneamente.
El servicio se registra en IHostBuilder.ConfigureServices
(Program.cs
) con el método de extensión AddHostedService
:
services.AddHostedService<TimedHostedService>();
Consumir un servicio con ámbito en una tarea en segundo plano
Para usar servicios con ámbito dentro de un BackgroundService, cree un ámbito. No se crean ámbitos de forma predeterminada para los servicios hospedados.
El servicio de tareas en segundo plano con ámbito contiene la lógica de la tarea en segundo plano. En el ejemplo siguiente:
- El servicio es asincrónico. El método
DoWork
devuelve un objetoTask
. Para fines de demostración, se espera un retraso de diez segundos en el métodoDoWork
. - ILogger se inserta en el servicio.
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);
}
}
}
El servicio hospedado crea un ámbito con el fin de resolver el servicio de tareas en segundo plano con ámbito para llamar a su método DoWork
. DoWork
devuelve Task
, que se espera en ExecuteAsync
:
public class ConsumeScopedServiceHostedService : BackgroundService
{
private readonly ILogger<ConsumeScopedServiceHostedService> _logger;
public ConsumeScopedServiceHostedService(IServiceProvider services,
ILogger<ConsumeScopedServiceHostedService> logger)
{
Services = services;
_logger = logger;
}
public IServiceProvider Services { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service running.");
await DoWork(stoppingToken);
}
private async Task DoWork(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is working.");
using (var scope = Services.CreateScope())
{
var scopedProcessingService =
scope.ServiceProvider
.GetRequiredService<IScopedProcessingService>();
await scopedProcessingService.DoWork(stoppingToken);
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is stopping.");
await base.StopAsync(stoppingToken);
}
}
Los servicios se registran en IHostBuilder.ConfigureServices
(Program.cs
). El servicio hospedado se registra en con el método de extensión AddHostedService
:
services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
Tareas en segundo plano en cola
Una cola de tareas en segundo plano se basa en QueueBackgroundWorkItem de .NET 4.x:
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;
}
}
En el ejemplo QueueHostedService
siguiente:
- El método
BackgroundProcessing
devuelveTask
, que se espera enExecuteAsync
. - Las tareas en segundo plano que están en cola se quitan de ella y se ejecutan en
BackgroundProcessing
. - Se esperan elementos de trabajo antes de que el servicio se detenga en
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);
}
}
Un servicio MonitorLoop
controla las tareas de puesta en cola para el servicio hospedado cada vez que se selecciona la tecla w
en un dispositivo de entrada:
IBackgroundTaskQueue
se inserta en el servicioMonitorLoop
.- Se llama a
IBackgroundTaskQueue.QueueBackgroundWorkItem
para poner en cola el elemento de trabajo. - El elemento de trabajo simula una tarea en segundo plano de larga duración:
- Se ejecutan tres retrasos de 5 segundos (
Task.Delay
). - Una instrucción
try-catch
captura OperationCanceledException si se cancela la tarea.
- Se ejecutan tres retrasos de 5 segundos (
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);
}
}
}
Los servicios se registran en IHostBuilder.ConfigureServices
(Program.cs
). El servicio hospedado se registra en con el método de extensión 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
se inicia en Program.cs
:
var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();
Tarea en segundo plano asincrónica temporizada
El código siguiente crea una tarea en segundo plano asincrónica temporizada:
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 nativo
Las plantillas de servicio de trabajo admiten .NET ahead-of-time (AOT) nativa con la marca --aot
:
- Cree un nuevo proyecto.
- Seleccione Worker Service (Servicio de Worker). Seleccione Siguiente.
- Proporcione un nombre para el proyecto en el campo Nombre del proyecto o acepte el predeterminado. Seleccione Next (Siguiente).
- En el cuadro de diálogo Información adicional:
- Seleccione un marco.
- Active la casilla Habilitar publicación nativa mediante AOT.
- Seleccione Crear.
La opción AOT agrega <PublishAot>true</PublishAot>
al archivo del proyecto:
<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>
Recursos adicionales
En ASP.NET Core, las tareas en segundo plano se pueden implementar como servicios hospedados. Un servicio hospedado es una clase con lógica de tarea en segundo plano que implementa la interfaz IHostedService. En este artículo se incluyen tres ejemplos de servicio hospedado:
- Una tarea en segundo plano que se ejecuta según un temporizador.
- Un servicio hospedado que activa un servicio con ámbito. El servicio con ámbito puede usar la inserción de dependencias (DI).
- Tareas en segundo plano en cola que se ejecutan en secuencia.
Plantilla Worker Service
La plantilla Worker Service de ASP.NET Core sirve de punto de partida para escribir aplicaciones de servicio de larga duración. Una aplicación creada a partir de la plantilla Worker Service especifica el SDK de trabajo en su archivo del proyecto:
<Project Sdk="Microsoft.NET.Sdk.Worker">
Para usar la plantilla como base de una aplicación de servicios hospedados:
- Cree un nuevo proyecto.
- Seleccione Worker Service (Servicio de Worker). Seleccione Siguiente.
- Proporcione un nombre para el proyecto en el campo Nombre del proyecto o acepte el predeterminado. Seleccione Siguiente.
- En el cuadro de diálogo Información adicional, elija Marco de trabajo. Seleccione Crear.
Paquete
Una aplicación basada en la plantilla Worker Service usa el SDK de Microsoft.NET.Sdk.Worker
y tiene una referencia de paquete explícita al paquete Microsoft.Extensions.Hosting. Por ejemplo, consulte el archivo del proyecto de la aplicación de ejemplo (BackgroundTasksSample.csproj
).
En el caso de las aplicaciones web que usan el SDK de Microsoft.NET.Sdk.Web
, desde el marco compartido se hace una referencia implícita al paquete Microsoft.Extensions.Hosting. No se requiere una referencia de paquete explícita en el archivo del proyecto de la aplicación.
Interfaz IHostedService
La interfaz IHostedService define dos métodos para los objetos administrados por el host:
StartAsync
StartAsync(CancellationToken) contiene la lógica para iniciar la tarea en segundo plano. Se llama a StartAsync
antes de que:
- La canalización de procesamiento de solicitudes de la aplicación está configurada.
- El servidor se haya iniciado y IApplicationLifetime.ApplicationStarted se haya activado.
StartAsync
debe limitarse a tareas de ejecución corta porque los servicios hospedados se ejecutan secuencialmente, y no se inician más servicios hasta que StartAsync
se ejecuta hasta su finalización.
StopAsync
- StopAsync(CancellationToken) se activa cuando el host está realizando un cierre estable.
StopAsync
contiene la lógica para finalizar la tarea en segundo plano. Implemente IDisposable y los finalizadores (destructores) para desechar los recursos no administrados.
El token de cancelación tiene un tiempo de espera predeterminado de 30 segundos para indicar que el proceso de cierre ya no debería ser estable. Cuando se solicita la cancelación en el token:
- Se deben anular las operaciones restantes en segundo plano que realiza la aplicación.
- Los métodos llamados en
StopAsync
deberían devolver contenido al momento.
Pero las tareas no se abandonan después de solicitar la cancelación, sino que el autor de la llamada espera a que se completen todas las tareas.
Si la aplicación se cierra inesperadamente (por ejemplo, porque se produzca un error en el proceso de la aplicación), puede que no sea posible llamar a StopAsync
. Por lo tanto, los métodos llamados o las operaciones llevadas a cabo en StopAsync
podrían no producirse.
Para ampliar el tiempo de espera predeterminado de apagado de 30 segundos, establezca:
- ShutdownTimeout cuando se usa el host genérico. Para obtener más información, consulte Hospedaje genérico de .NET en ASP.NET Core.
- Configuración de los valores de host de tiempo de espera de apagado cuando se usa el host web. Para obtener más información, consulte Host web de ASP.NET Core.
El servicio hospedado se activa una vez al inicio de la aplicación y se cierra de manera estable cuando dicha aplicación se cierra. Si se produce un error durante la ejecución de una tarea en segundo plano, hay que llamar a Dispose
, aun cuando no se haya llamado a StopAsync
.
Clase base BackgroundService
BackgroundService es una clase base para implementar un IHostedService de larga duración.
Se llama a ExecuteAsync(CancellationToken) para ejecutar el servicio en segundo plano. La implementación devuelve Task, que representa toda la duración del servicio en segundo plano. No se inicia ningún servicio hasta que ExecuteAsync se convierte en asincrónico, mediante una llamada a await
. Evite realizar un trabajo de inicialización de bloqueo prolongado en ExecuteAsync
. El host se bloquea en StopAsync(CancellationToken) a la espera de que ExecuteAsync
se complete.
El token de cancelación se desencadena cuando se llama a IHostedService.StopAsync. La implementación de ExecuteAsync
debe finalizar rápidamente cuando se active el token de cancelación para cerrar correctamente el servicio. De lo contrario, el servicio se cierra de manera abrupta durante el tiempo de expiración del cierre. Para más información, consulte la sección sobre la interfaz IHostedService.
Para obtener más información, vea el código fuente de BackgroundService.
Tareas en segundo plano temporizadas
Una tarea en segundo plano temporizada hace uso de la clase System.Threading.Timer. El temporizador activa el método DoWork
de la tarea. El temporizador está deshabilitado en StopAsync
y se desecha cuando el contenedor de servicios se elimina en 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 no espera a que finalicen las ejecuciones anteriores de DoWork
, por lo que es posible que el enfoque mostrado no sea adecuado para todos los escenarios. Interlocked.Increment se usa para incrementar el contador de ejecución como una operación atómica, lo que garantiza que varios subprocesos no actualicen executionCount
simultáneamente.
El servicio se registra en IHostBuilder.ConfigureServices
(Program.cs
) con el método de extensión AddHostedService
:
services.AddHostedService<TimedHostedService>();
Consumir un servicio con ámbito en una tarea en segundo plano
Para usar servicios con ámbito dentro de un BackgroundService, cree un ámbito. No se crean ámbitos de forma predeterminada para los servicios hospedados.
El servicio de tareas en segundo plano con ámbito contiene la lógica de la tarea en segundo plano. En el ejemplo siguiente:
- El servicio es asincrónico. El método
DoWork
devuelve un objetoTask
. Para fines de demostración, se espera un retraso de diez segundos en el métodoDoWork
. - ILogger se inserta en el servicio.
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);
}
}
}
El servicio hospedado crea un ámbito con el fin de resolver el servicio de tareas en segundo plano con ámbito para llamar a su método DoWork
. DoWork
devuelve Task
, que se espera en ExecuteAsync
:
public class ConsumeScopedServiceHostedService : BackgroundService
{
private readonly ILogger<ConsumeScopedServiceHostedService> _logger;
public ConsumeScopedServiceHostedService(IServiceProvider services,
ILogger<ConsumeScopedServiceHostedService> logger)
{
Services = services;
_logger = logger;
}
public IServiceProvider Services { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service running.");
await DoWork(stoppingToken);
}
private async Task DoWork(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is working.");
using (var scope = Services.CreateScope())
{
var scopedProcessingService =
scope.ServiceProvider
.GetRequiredService<IScopedProcessingService>();
await scopedProcessingService.DoWork(stoppingToken);
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is stopping.");
await base.StopAsync(stoppingToken);
}
}
Los servicios se registran en IHostBuilder.ConfigureServices
(Program.cs
). El servicio hospedado se registra en con el método de extensión AddHostedService
:
services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
Tareas en segundo plano en cola
Una cola de tareas en segundo plano se basa en QueueBackgroundWorkItem de .NET 4.x:
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;
}
}
En el ejemplo QueueHostedService
siguiente:
- El método
BackgroundProcessing
devuelveTask
, que se espera enExecuteAsync
. - Las tareas en segundo plano que están en cola se quitan de ella y se ejecutan en
BackgroundProcessing
. - Se esperan elementos de trabajo antes de que el servicio se detenga en
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);
}
}
Un servicio MonitorLoop
controla las tareas de puesta en cola para el servicio hospedado cada vez que se selecciona la tecla w
en un dispositivo de entrada:
IBackgroundTaskQueue
se inserta en el servicioMonitorLoop
.- Se llama a
IBackgroundTaskQueue.QueueBackgroundWorkItem
para poner en cola el elemento de trabajo. - El elemento de trabajo simula una tarea en segundo plano de larga duración:
- Se ejecutan tres retrasos de 5 segundos (
Task.Delay
). - Una instrucción
try-catch
captura OperationCanceledException si se cancela la tarea.
- Se ejecutan tres retrasos de 5 segundos (
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);
}
}
}
Los servicios se registran en IHostBuilder.ConfigureServices
(Program.cs
). El servicio hospedado se registra en con el método de extensión 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
se inicia en Program.cs
:
var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();
Tarea en segundo plano asincrónica temporizada
El código siguiente crea una tarea en segundo plano asincrónica temporizada:
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);
}
}
Recursos adicionales
En ASP.NET Core, las tareas en segundo plano se pueden implementar como servicios hospedados. Un servicio hospedado es una clase con lógica de tarea en segundo plano que implementa la interfaz IHostedService. En este artículo se incluyen tres ejemplos de servicio hospedado:
- Una tarea en segundo plano que se ejecuta según un temporizador.
- Un servicio hospedado que activa un servicio con ámbito. El servicio con ámbito puede usar la inserción de dependencias (DI).
- Tareas en segundo plano en cola que se ejecutan en secuencia.
Vea o descargue el código de ejemplo (cómo descargarlo)
Plantilla Worker Service
La plantilla Worker Service de ASP.NET Core sirve de punto de partida para escribir aplicaciones de servicio de larga duración. Una aplicación creada a partir de la plantilla Worker Service especifica el SDK de trabajo en su archivo del proyecto:
<Project Sdk="Microsoft.NET.Sdk.Worker">
Para usar la plantilla como base de una aplicación de servicios hospedados:
- Cree un nuevo proyecto.
- Seleccione Worker Service (Servicio de Worker). Seleccione Siguiente.
- Proporcione un nombre para el proyecto en el campo Nombre del proyecto o acepte el predeterminado. Seleccione Crear.
- En el cuadro de diálogo Crear un servicio de Worker, seleccione Crear.
Paquete
Una aplicación basada en la plantilla Worker Service usa el SDK de Microsoft.NET.Sdk.Worker
y tiene una referencia de paquete explícita al paquete Microsoft.Extensions.Hosting. Por ejemplo, consulte el archivo del proyecto de la aplicación de ejemplo (BackgroundTasksSample.csproj
).
En el caso de las aplicaciones web que usan el SDK de Microsoft.NET.Sdk.Web
, desde el marco compartido se hace una referencia implícita al paquete Microsoft.Extensions.Hosting. No se requiere una referencia de paquete explícita en el archivo del proyecto de la aplicación.
Interfaz IHostedService
La interfaz IHostedService define dos métodos para los objetos administrados por el host:
StartAsync
StartAsync
contiene la lógica para iniciar la tarea en segundo plano. Se llama a StartAsync
antes de que:
- La canalización de procesamiento de solicitudes de la aplicación está configurada.
- El servidor se haya iniciado y IApplicationLifetime.ApplicationStarted se haya activado.
El comportamiento predeterminado se puede cambiar para que el StartAsync
del servicio hospedado se ejecute después de que se haya configurado la canalización de la aplicación y se haya llamado a ApplicationStarted
. Para cambiar el comportamiento predeterminado, agregue el servicio hospedado (VideosWatcher
en el ejemplo siguiente) después de llamar a ConfigureWebHostDefaults
:
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.ConfigureServices(services =>
{
services.AddHostedService<VideosWatcher>();
});
}
StopAsync
- StopAsync(CancellationToken) se activa cuando el host está realizando un cierre estable.
StopAsync
contiene la lógica para finalizar la tarea en segundo plano. Implemente IDisposable y los finalizadores (destructores) para desechar los recursos no administrados.
El token de cancelación tiene un tiempo de espera predeterminado de cinco segundos para indicar que el proceso de cierre ya no debería ser estable. Cuando se solicita la cancelación en el token:
- Se deben anular las operaciones restantes en segundo plano que realiza la aplicación.
- Los métodos llamados en
StopAsync
deberían devolver contenido al momento.
Pero las tareas no se abandonan después de solicitar la cancelación, sino que el autor de la llamada espera a que se completen todas las tareas.
Si la aplicación se cierra inesperadamente (por ejemplo, porque se produzca un error en el proceso de la aplicación), puede que no sea posible llamar a StopAsync
. Por lo tanto, los métodos llamados o las operaciones llevadas a cabo en StopAsync
podrían no producirse.
Para ampliar el tiempo de espera predeterminado de apagado de 5 segundos, establezca:
- ShutdownTimeout cuando se usa el host genérico. Para obtener más información, consulte Hospedaje genérico de .NET en ASP.NET Core.
- Configuración de los valores de host de tiempo de espera de apagado cuando se usa el host web. Para obtener más información, consulte Host web de ASP.NET Core.
El servicio hospedado se activa una vez al inicio de la aplicación y se cierra de manera estable cuando dicha aplicación se cierra. Si se produce un error durante la ejecución de una tarea en segundo plano, hay que llamar a Dispose
, aun cuando no se haya llamado a StopAsync
.
Clase base BackgroundService
BackgroundService es una clase base para implementar un IHostedService de larga duración.
Se llama a ExecuteAsync(CancellationToken) para ejecutar el servicio en segundo plano. La implementación devuelve Task, que representa toda la duración del servicio en segundo plano. No se inicia ningún servicio hasta que ExecuteAsync se convierte en asincrónico, mediante una llamada a await
. Evite realizar un trabajo de inicialización de bloqueo prolongado en ExecuteAsync
. El host se bloquea en StopAsync(CancellationToken) a la espera de que ExecuteAsync
se complete.
El token de cancelación se desencadena cuando se llama a IHostedService.StopAsync. La implementación de ExecuteAsync
debe finalizar rápidamente cuando se active el token de cancelación para cerrar correctamente el servicio. De lo contrario, el servicio se cierra de manera abrupta durante el tiempo de expiración del cierre. Para más información, consulte la sección sobre la interfaz IHostedService.
StartAsync
debe limitarse a tareas de ejecución corta porque los servicios hospedados se ejecutan secuencialmente, y no se inician más servicios hasta que StartAsync
se ejecuta hasta su finalización. Las tareas de larga duración deben colocarse en ExecuteAsync
. Para obtener más información, vea el origen para BackgroundService.
Tareas en segundo plano temporizadas
Una tarea en segundo plano temporizada hace uso de la clase System.Threading.Timer. El temporizador activa el método DoWork
de la tarea. El temporizador está deshabilitado en StopAsync
y se desecha cuando el contenedor de servicios se elimina en 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 no espera a que finalicen las ejecuciones anteriores de DoWork
, por lo que es posible que el enfoque mostrado no sea adecuado para todos los escenarios. Interlocked.Increment se usa para incrementar el contador de ejecución como una operación atómica, lo que garantiza que varios subprocesos no actualicen executionCount
simultáneamente.
El servicio se registra en IHostBuilder.ConfigureServices
(Program.cs
) con el método de extensión AddHostedService
:
services.AddHostedService<TimedHostedService>();
Consumir un servicio con ámbito en una tarea en segundo plano
Para usar servicios con ámbito dentro de un BackgroundService, cree un ámbito. No se crean ámbitos de forma predeterminada para los servicios hospedados.
El servicio de tareas en segundo plano con ámbito contiene la lógica de la tarea en segundo plano. En el ejemplo siguiente:
- El servicio es asincrónico. El método
DoWork
devuelve un objetoTask
. Para fines de demostración, se espera un retraso de diez segundos en el métodoDoWork
. - ILogger se inserta en el servicio.
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);
}
}
}
El servicio hospedado crea un ámbito con el fin de resolver el servicio de tareas en segundo plano con ámbito para llamar a su método DoWork
. DoWork
devuelve Task
, que se espera en ExecuteAsync
:
public class ConsumeScopedServiceHostedService : BackgroundService
{
private readonly ILogger<ConsumeScopedServiceHostedService> _logger;
public ConsumeScopedServiceHostedService(IServiceProvider services,
ILogger<ConsumeScopedServiceHostedService> logger)
{
Services = services;
_logger = logger;
}
public IServiceProvider Services { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service running.");
await DoWork(stoppingToken);
}
private async Task DoWork(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is working.");
using (var scope = Services.CreateScope())
{
var scopedProcessingService =
scope.ServiceProvider
.GetRequiredService<IScopedProcessingService>();
await scopedProcessingService.DoWork(stoppingToken);
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is stopping.");
await base.StopAsync(stoppingToken);
}
}
Los servicios se registran en IHostBuilder.ConfigureServices
(Program.cs
). El servicio hospedado se registra en con el método de extensión AddHostedService
:
services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
Tareas en segundo plano en cola
Una cola de tareas en segundo plano se basa en QueueBackgroundWorkItem de .NET 4.x:
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;
}
}
En el ejemplo QueueHostedService
siguiente:
- El método
BackgroundProcessing
devuelveTask
, que se espera enExecuteAsync
. - Las tareas en segundo plano que están en cola se quitan de ella y se ejecutan en
BackgroundProcessing
. - Se esperan elementos de trabajo antes de que el servicio se detenga en
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);
}
}
Un servicio MonitorLoop
controla las tareas de puesta en cola para el servicio hospedado cada vez que se selecciona la tecla w
en un dispositivo de entrada:
IBackgroundTaskQueue
se inserta en el servicioMonitorLoop
.- Se llama a
IBackgroundTaskQueue.QueueBackgroundWorkItem
para poner en cola el elemento de trabajo. - El elemento de trabajo simula una tarea en segundo plano de larga duración:
- Se ejecutan tres retrasos de 5 segundos (
Task.Delay
). - Una instrucción
try-catch
captura OperationCanceledException si se cancela la tarea.
- Se ejecutan tres retrasos de 5 segundos (
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);
}
}
}
Los servicios se registran en IHostBuilder.ConfigureServices
(Program.cs
). El servicio hospedado se registra en con el método de extensión 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
se inicia en Program.Main
:
var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();