큐 서비스는 이전 작업 항목이 완료될 때 작업 항목을 큐에 대기하고 순차적으로 작업할 수 있는 장기 실행 서비스의 좋은 예입니다. 작업자 서비스 템플릿을 기반으로 하여 BackgroundService에 새로운 기능을 추가합니다.
이 튜토리얼에서는 다음을 배우게 됩니다:
- 큐 서비스를 만듭니다.
- 작업 큐에 작업을 위임합니다.
- IHostApplicationLifetime 이벤트에 대한 콘솔 키 수신기를 등록합니다.
팁 (조언)
".NET의 작업자" 예제 소스 코드는 모두 샘플 브라우저 에서 다운로드할 수 있습니다. 자세한 내용은 코드 샘플 찾아보기: .NET 작업자를 참조하세요.
필수 조건
- .NET 8.0 SDK 버전 이상
- .NET IDE(통합 개발 환경)
새 프로젝트 만들기
Visual Studio를 사용하여 새 작업자 서비스 프로젝트를 만들려면 파일>새>Project...선택합니다. 새 프로젝트 만들기 대화 상자에서 "작업자 서비스"를 검색하고 작업자 서비스 템플릿을 선택합니다. .NET CLI를 사용하려는 경우 작업 디렉터리에서 즐겨 찾는 터미널을 엽니다.
dotnet new 명령을 실행하고 <Project.Name>을(를) 원하는 프로젝트 이름으로 바꿉니다.
dotnet new worker --name <Project.Name>
.NET CLI 새 작업자 서비스 프로젝트 명령에 대한 자세한 내용은 dotnet new Worker를 참조하세요.
팁 (조언)
Visual Studio Code를 사용하는 경우 통합 터미널에서 .NET CLI 명령을 실행할 수 있습니다. 자세한 내용은 Visual Studio Code: 통합 터미널을 참조하세요.
대기열 서비스 만들기
System.Web.Hosting 네임스페이스의 QueueBackgroundWorkItem(Func<CancellationToken,Task>) 기능에 익숙할 수 있습니다.
팁 (조언)
네임스페이 System.Web 스의 기능은 의도적으로 .NET으로 이식되지 않았으며 .NET Framework 전용으로 유지됩니다. 자세한 내용은 ASP.NET을 ASP.NET Core로 점진적으로 마이그레이션 시작하기를 참조하세요.
.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.Wait는 ChannelWriter<T>.WriteAsync 호출 시 공간이 확보될 때까지 완료되지 않는 작업을 반환합니다. 너무 많은 게시자/호출이 누적되는 경우 백프레셔로 이어집니다.
작업자 클래스 다시 쓰기
다음 QueueHostedService 예제에서
-
ProcessTaskQueueAsync메서드는 Task의ExecuteAsync을 반환합니다. -
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);
}
}
입력 장치에서 MonitorLoop 키가 선택될 때마다 w 서비스가 호스팅된 서비스에 대해 큐에 넣는 작업을 처리합니다.
-
IBackgroundTaskQueue가MonitorLoop서비스에 삽입됩니다. -
IBackgroundTaskQueue.QueueBackgroundWorkItemAsync이 호출되어 작업 항목을 큐에 넣습니다. - 작업 항목은 장기 실행 백그라운드 작업을 시뮬레이션합니다.
- 3개의 5초 지연이 실행됩니다 Delay.
- 작업이 취소되면
try-catch문이 OperationCanceledException를 가로챕니다.
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 확장 메서드를 사용하여 등록됩니다.
MonitorLoop 는 Program.cs 최상위 문에서 시작됩니다.
MonitorLoop monitorLoop = host.Services.GetRequiredService<MonitorLoop>()!;
monitorLoop.StartMonitorLoop();
서비스 등록에 대한 자세한 내용은 .NET 종속성 주입을 참조하세요.
서비스 기능 확인
Visual Studio에서 애플리케이션을 실행하려면 F5 를 선택하거나 디버그>시작 디버깅 메뉴 옵션을 선택합니다. .NET CLI를 사용하는 경우 작업 디렉터리에서 명령을 실행 dotnet run 합니다.
dotnet run
.NET CLI 실행 명령에 대한 자세한 내용은 dotnet run을 참조하세요.
메시지가 표시되면 예제 출력에 나와 있는 것처럼 에뮬레이트된 작업 항목을 큐에 넣으려면 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 를 선택하여 취소 신호를 표시합니다.
참고하십시오
.NET