Share via


큐 서비스 만들기

큐 서비스는 이전 작업 항목이 완료될 때 작업 항목을 큐에 대기하고 순차적으로 작업할 수 있는 장기 실행 서비스의 좋은 예제입니다. Worker Service 템플릿을 기반으로 하여 BackgroundService 위에 새로운 기능을 빌드합니다.

이 자습서에서는 다음을 하는 방법을 알아볼 수 있습니다.

  • 큐 서비스를 만듭니다.
  • 작업 큐에 작업을 위임합니다.
  • IHostApplicationLifetime 이벤트에서 콘솔 키 수신기를 등록합니다.

모든 ‘.NET의 작업자’ 예제 소스 코드는 샘플 브라우저에서 다운로드할 수 있습니다. 자세한 내용은 코드 샘플 찾아보기: .NET의 작업자를 참조하세요.

필수 조건

새 프로젝트 만들기

Visual Studio를 사용하여 새 Worker Service 프로젝트를 만들려면 파일>새로 만들기>Project...를 선택합니다. 새 프로젝트 만들기 대화 상자에서 "Worker Service"를 검색하고 Worker Service 템플릿을 선택합니다. .NET CLI를 사용하려면 작업 디렉터리에서 즐겨찾는 터미널을 엽니다. dotnet new 명령을 실행하고 <Project.Name>을 원하는 프로젝트 이름으로 바꿉니다.

dotnet new worker --name <Project.Name>

.NET CLI 새 작업자 서비스 프로젝트 명령에 대한 자세한 내용은 dotnet 새 작업자를 참조하세요.

Visual Studio Code를 사용하는 경우 통합 터미널에서 .NET CLI 명령을 실행할 수 있습니다. 자세한 내용은 Visual Studio Code: 통합 터미널을 참조하세요.

큐 서비스 만들기

System.Web.Hosting 네임스페이스의 QueueBackgroundWorkItem(Func<CancellationToken,Task>) 기능에 대해 잘 알고 있을 수 있습니다.

System.Web 네임스페이스의 기능은 의도적으로 .NET으로 포팅되지 않았으며 .NET Framework 전용으로 유지됩니다. 자세한 내용은 ASP.NET Core 마이그레이션을 위한 증분 ASP.NET 시작하기를 참조하세요.

.NET에서 QueueBackgroundWorkItem 기능으로 영향을 받은 서비스를 모델링하려면 먼저 프로젝트에 IBackgroundTaskQueue 인터페이스를 추가합니다.

namespace App.QueueService;

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

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

큐 기능을 노출하는 메서드와 이전에 큐에 대기 중인 작업 항목을 큐에서 제거하는 두 가지 방법이 있습니다. 작업 항목Func<CancellationToken, ValueTask>입니다. 다음으로 프로젝트에 기본 구현을 추가합니다.

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

위의 구현은 Channel<T>을 큐로 사용합니다. BoundedChannelOptions(Int32)는 명시적 용량을 사용하여 호출됩니다. 예상되는 애플리케이션 부하 및 큐에 액세스하는 동시 스레드 수를 기반으로 용량을 설정해야 합니다. BoundedChannelFullMode.WaitChannelWriter<T>.WriteAsync에 대한 호출로 공간을 사용할 수 있을 때만 완료되는 작업을 반환하도록 합니다. 너무 많은 게시자/호출이 시작되는 경우에는 이로 인해 역압이 발생합니다.

Worker 클래스 다시 작성

다음 QueueHostedService 예제에서

  • ProcessTaskQueueAsync 메서드는 ExecuteAsync에서 Task를 반환합니다.
  • ProcessTaskQueueAsync에서 큐의 백그라운드 작업이 큐에서 제거되고 실행됩니다.
  • 작업 항목은 StopAsync에서 서비스가 중지되기 전에 대기 상태입니다.

기존 Worker 클래스를 다음 C# 코드로 바꾸고 파일 이름을 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);
    }
}

입력 장치에서 w 키가 선택될 때마다 MonitorLoop 서비스가 호스팅된 서비스에 대해 큐에 넣는 작업을 처리합니다.

  • IBackgroundTaskQueueMonitorLoop 서비스에 삽입됩니다.
  • IBackgroundTaskQueue.QueueBackgroundWorkItemAsync이 호출되어 작업 항목을 큐에 넣습니다.
  • 작업 항목은 장기 실행 백그라운드 작업을 시뮬레이션합니다.
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);
        }
    }
}

기존 Program 콘텐츠를 다음 C# 코드로 바꿉니다.

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

서비스는 (Program.cs)에 등록됩니다. 호스팅된 서비스는 AddHostedService 확장 메서드를 사용하여 등록됩니다. MonitorLoopProgram.cs의 최상위 수준 명령문에서 시작됩니다.

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

서비스 등록에 대한 자세한 내용은 .NET에서 종속성 주입을 참조하세요.

서비스 기능 확인

Visual Studio에서 애플리케이션을 실행하려면 F5를 선택하거나 디버그>디버깅 시작 메뉴 옵션을 선택합니다. .NET CLI를 사용하는 경우 작업 디렉터리에서 dotnet run 명령을 실행합니다.

dotnet run

.NET CLI 실행 명령에 대한 자세한 내용은 dotnet 실행을 참조하세요.

메시지가 표시되면 예제 출력과 같이 w(또는 W)를 한 번 이상 입력하여 에뮬레이트된 작업 항목을 큐에 대기합니다.

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.

Visual Studio 내에서 애플리케이션을 실행하는 경우 디버그>디버깅 중지...를 선택합니다. 또는 콘솔 창에서 Ctrl + C를 선택하여 취소 신호를 보냅니다.

참고 항목