Créer un service de file d’attente

Un service de file d’attente est un excellent exemple de service de longue durée, où les éléments de travail peuvent être mis en file d’attente et travaillés de manière séquentielle à mesure que les éléments de travail précédents sont terminés. En vous appuyant sur le modèle Worker Service, vous générez de nouvelles fonctionnalités par-dessus le BackgroundService.

Dans ce tutoriel, vous allez apprendre à :

  • Créer un service de file d’attente.
  • Déléguer le travail à une file d’attente de tâches.
  • Inscrire un écouteur de clé de console à partir d’événements IHostApplicationLifetime .

Conseil

Tout le code source d’exemple « Workers dans .NET » peut être téléchargé dans l’Explorateur d’exemples. Pour plus d’informations, consultez Parcourir les exemples de code : Workers dans .NET.

Prérequis

Création d'un projet

Pour créer un projet de service Worker avec Visual Studio, vous devez sélectionner Fichier>Nouveau>Projet.... Dans la boîte de dialogue Créer un projet, recherchez « Worker Service », puis sélectionnez Modèle Worker Service. Si vous préférez utiliser l’interface CLI .NET, ouvrez votre terminal favori dans un répertoire de travail. Exécutez la commande dotnet new et remplacez le <Project.Name> par le nom de projet souhaité.

dotnet new worker --name <Project.Name>

Pour plus d’informations sur la commande de projet de service new worker de l’interface CLI .NET, consultez dotnet new worker.

Conseil

Si vous utilisez Visual Studio Code, vous pouvez exécuter des commandes CLI .NET à partir du terminal intégré. Pour plus d’informations, consultez Visual Studio Code : Terminal intégré.

Créer des services de mise en file d’attente

Vous connaissez peut-être la fonctionnalité QueueBackgroundWorkItem(Func<CancellationToken,Task>) de l’espace de noms System.Web.Hosting.

Conseil

Les fonctionnalités de l’espace de noms System.Web n’ont pas été transférées intentionnellement vers .NET et restent exclusives au .NET Framework. Pour plus d’informations, consultez Prise en main de la migration incrémentielle d’ASP.NET vers ASP.NET Core.

Dans .NET, pour modéliser un service qui s’inspire de la fonctionnalité QueueBackgroundWorkItem, commencez par ajouter une interface IBackgroundTaskQueue au projet :

namespace App.QueueService;

public interface IBackgroundTaskQueue
{
    ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem);

    ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken);
}

Il existe deux méthodes : l’une qui expose la fonctionnalité de mise en file d’attente et l’autre qui supprime les éléments de travail précédemment mis en file d’attente. Un élément de travail est une Func<CancellationToken, ValueTask>. Ajoutez ensuite l’implémentation par défaut au projet.

using System.Threading.Channels;

namespace App.QueueService;

public sealed class DefaultBackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

    public DefaultBackgroundTaskQueue(int capacity)
    {
        BoundedChannelOptions options = new(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
    }

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        ArgumentNullException.ThrowIfNull(workItem);

        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        Func<CancellationToken, ValueTask>? workItem =
            await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}

L’implémentation précédente s’appuie sur un Channel<T> en tant que file d’attente. Le BoundedChannelOptions(Int32) est appelé avec une capacité explicite. La capacité doit être définie en fonction de la charge d’application attendue et du nombre de threads simultanés accédant à la file d’attente. BoundedChannelFullMode.Wait entraîne les appels à ChannelWriter<T>.WriteAsync pour renvoyer une tâche, qui ne se termine que lorsque l’espace est disponible. Ce qui entraîne une contre-pression, au cas où trop d’éditeurs/appels commenceraient à s’accumuler.

Réécrire la classe Worker

Dans l’exemple QueueHostedService suivant :

  • La méthode ProcessTaskQueueAsync retourne une Task en ExecuteAsync.
  • Les tâches d’arrière-plan de la file d’attente sont sorties de la file et exécutées dans ProcessTaskQueueAsync.
  • Les éléments de travail sont attendus avant que le service s’arrête dans StopAsync.

Remplacez la classe Worker existante par le code C# suivant et renommez le fichier QueueHostedService.cs.

namespace App.QueueService;

public sealed class QueuedHostedService(
        IBackgroundTaskQueue taskQueue,
        ILogger<QueuedHostedService> logger) : BackgroundService
{
    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation("""
            {Name} is running.
            Tap W to add a work item to the 
            background queue.
            """,
            nameof(QueuedHostedService));

        return ProcessTaskQueueAsync(stoppingToken);
    }

    private async Task ProcessTaskQueueAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                Func<CancellationToken, ValueTask>? workItem =
                    await taskQueue.DequeueAsync(stoppingToken);

                await workItem(stoppingToken);
            }
            catch (OperationCanceledException)
            {
                // Prevent throwing if stoppingToken was signaled
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Error occurred executing task work item.");
            }
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation(
            $"{nameof(QueuedHostedService)} is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

Un service MonitorLoop gère les tâches de mise en file d’attente pour le service hébergé chaque fois que la clé w est sélectionnée sur un périphérique d’entrée :

  • La IBackgroundTaskQueue est injectée dans le service MonitorLoop.
  • IBackgroundTaskQueue.QueueBackgroundWorkItemAsync est appelée pour mettre un élément de travail en file d’attente.
  • L’élément de travail simule une tâche en arrière-plan de longue durée :
namespace App.QueueService;

public sealed class MonitorLoop(
    IBackgroundTaskQueue taskQueue,
    ILogger<MonitorLoop> logger,
    IHostApplicationLifetime applicationLifetime)
{
    private readonly CancellationToken _cancellationToken = applicationLifetime.ApplicationStopping;

    public void StartMonitorLoop()
    {
        logger.LogInformation($"{nameof(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(BuildWorkItemAsync);
            }
        }
    }

    private async ValueTask BuildWorkItemAsync(CancellationToken token)
    {
        // Simulate three 5-second tasks to complete
        // for each enqueued work item

        int delayLoop = 0;
        var guid = Guid.NewGuid();

        logger.LogInformation("Queued work item {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 work item {Guid} is running. {DelayLoop}/3", guid, delayLoop);
        }

        if (delayLoop is 3)
        {
            logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
        }
        else
        {
            logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
        }
    }
}

Remplacez le contenu Program existant par le code C# suivant :

using App.QueueService;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<MonitorLoop>();
builder.Services.AddHostedService<QueuedHostedService>();
builder.Services.AddSingleton<IBackgroundTaskQueue>(_ => 
{
    if (!int.TryParse(builder.Configuration["QueueCapacity"], out var queueCapacity))
    {
        queueCapacity = 100;
    }

    return new DefaultBackgroundTaskQueue(queueCapacity);
});

IHost host = builder.Build();

MonitorLoop monitorLoop = host.Services.GetRequiredService<MonitorLoop>()!;
monitorLoop.StartMonitorLoop();

host.Run();

Les services sont inscrits dans (Program.cs). Le service hébergé est inscrit avec la méthode d’extension AddHostedService. MonitorLoop est démarré dans l’instruction de niveau supérieur Program.cs :

MonitorLoop monitorLoop = host.Services.GetRequiredService<MonitorLoop>()!;
monitorLoop.StartMonitorLoop();

Pour plus d’informations sur l’inscription de services, consultez Injection de dépendances dans .NET.

Vérifier les fonctionnalités du service

Pour exécuter l’application à partir de Visual Studio, sélectionnez F5 ou sélectionnez l’option de menu Déboguer>Démarrer le débogage. Si vous utilisez l’interface CLI .NET, exécutez la commande dotnet run à partir du répertoire de travail :

dotnet run

Pour plus d’informations sur la commande d’exécution de l’interface CLI .NET, consultez dotnet run.

Lorsque vous y êtes invité, entrez le w (ou W) au moins une fois pour mettre en file d’attente un élément de travail émulé, comme indiqué dans l’exemple de sortie :

info: App.QueueService.MonitorLoop[0]
      MonitorAsync loop is starting.
info: App.QueueService.QueuedHostedService[0]
      QueuedHostedService is running.

      Tap W to add a work item to the background queue.

info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: .\queue-service
winfo: App.QueueService.MonitorLoop[0]
      Queued work item 8453f845-ea4a-4bcb-b26e-c76c0d89303e is starting.
info: App.QueueService.MonitorLoop[0]
      Queued work item 8453f845-ea4a-4bcb-b26e-c76c0d89303e is running. 1/3
info: App.QueueService.MonitorLoop[0]
      Queued work item 8453f845-ea4a-4bcb-b26e-c76c0d89303e is running. 2/3
info: App.QueueService.MonitorLoop[0]
      Queued work item 8453f845-ea4a-4bcb-b26e-c76c0d89303e is running. 3/3
info: App.QueueService.MonitorLoop[0]
      Queued Background Task 8453f845-ea4a-4bcb-b26e-c76c0d89303e is complete.
info: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
info: App.QueueService.QueuedHostedService[0]
      QueuedHostedService is stopping.

Si vous exécutez l’application à partir de Visual Studio, sélectionnez Déboguer>Arrêter le débogage.... Vous pouvez également sélectionner Ctrl + C dans la fenêtre de console pour signaler l’annulation.

Voir aussi