ASP.NET Core에서 호스팅되는 서비스를 사용하는 백그라운드 작업

작성자: Jeow Li Huan

참고 항목

이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.

Important

이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.

현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.

ASP.NET Core에서 백그라운드 작업은 호스팅되는 서비스로 구현될 수 있습니다. 호스티드 서비스는 IHostedService 인터페이스를 구현하는 백그라운드 작업 논리가 있는 클래스입니다. 이 문서에서는 다음과 같이 호스티드 서비스 예를 세 가지 제공합니다.

  • 타이머에서 실행되는 백그라운드 작업.
  • 범위가 지정된 서비스를 활성화하는 호스팅되는 서비스. 범위가 지정된 서비스는 종속성 주입(DI)을 사용할 수 있습니다.
  • 순차적으로 실행되는 대기 중인 백그라운드 작업.

Worker Service 템플릿

ASP.NET Core Worker Service 템플릿은 장기간 실행되는 서비스 앱을 작성하기 위한 시작점을 제공합니다. Worker Service 템플릿에서 만든 앱은 프로젝트 파일에 작업자 SDK를 지정합니다.

<Project Sdk="Microsoft.NET.Sdk.Worker">

템플릿을 호스팅되는 서비스 앱의 기반으로 사용하려면 다음을 수행합니다.

  1. 새 프로젝트를 만듭니다.
  2. 작업자 서비스 선택합니다. 다음을 선택합니다.
  3. 프로젝트 이름 필드에 프로젝트 이름을 제공하거나 기본 프로젝트 이름을 수락합니다. 다음을 선택합니다.
  4. 추가 정보 대화 상자에서 프레임워크를 선택합니다. 만들기를 실행합니다.

Package(패키지)

Worker Service 템플릿을 기반으로 하는 앱은 Microsoft.NET.Sdk.Worker SDK를 사용하고 Microsoft.Extensions.Hosting 패키지에 대한 명시적 패키지 참조가 있습니다. 예를 들어 샘플 앱의 프로젝트 파일(BackgroundTasksSample.csproj)을 참조하세요.

Microsoft.NET.Sdk.Web SDK를 사용하는 웹앱의 경우, Microsoft.Extensions.Hosting 패키지는 공유 프레임워크에서 암시적으로 참조됩니다. 앱의 프로젝트 파일에 있는 명시적 패키지 참조는 필요하지 않습니다.

IHostedService 인터페이스

IHostedService 인터페이스는 호스트에 의해 관리되는 개체에 대한 두 가지 메서드를 정의합니다.

StartAsync

StartAsync(CancellationToken)에는 백그라운드 작업을 시작하는 논리가 포함됩니다. 다음 상황 이전StartAsync가 호출됩니다.

StartAsync는 호스트된 서비스가 순차적으로 실행되고 StartAsync 실행이 완료될 때까지 추가 서비스가 시작되지 않으므로 단기 실행 작업으로 제한되어야 합니다.

StopAsync

취소 토큰에는 종료 프로세스가 더 이상 정상이 아니어야 함을 나타내는 기본 30초 제한 시간이 있습니다. 토큰에 취소가 요청된 경우:

  • 앱이 수행하는 나머지 백그라운드 작업은 중단해야 합니다.
  • StopAsync에 호출된 모든 메서드는 즉시 반환해야 합니다.

그러나 취소가 요청된 후 호출자가 모든 작업을 완료될 때까지 작업을 취소하지 않습니다.

앱이 예기치 않게 종료된 경우(예: 앱의 프로세스가 실패한 경우), StopAsync가 호출되지 않을 수 있습니다. 따라서 메서드 호출이나 StopAsync에서 수행된 작업이 발생하지 않을 수 있습니다.

기본 30초 종료 시간 제한을 연장하려면 다음을 설정합니다.

호스팅되는 서비스는 앱 시작 시 한 번 활성화되고 앱 종료 시 정상적으로 종료됩니다. 백그라운드 작업을 실행하는 동안 오류가 발생하면 StopAsync가 호출되지 않아도 Dispose를 호출해야 합니다.

BackgroundService 기본 클래스

BackgroundService는 장기 실행 IHostedService를 구현하기 위한 기본 클래스입니다.

ExecuteAsync(CancellationToken)은 백그라운드 서비스를 실행하기 위해 호출됩니다. 구현하면 백그라운드 서비스의 전체 수명을 나타내는 Task가 반환됩니다. await를 호출하는 것처럼 ExecuteAsync가 비동기가 될 때까지 추가 서비스가 시작되지 않습니다. ExecuteAsync에서 긴 초기화 작업을 차단하는 것을 방지합니다. StopAsync(CancellationToken)의 호스트 블록은 ExecuteAsync가 완료될 때까지 대기합니다.

취소 토큰은 IHostedService.StopAsync가 호출되면 트리거됩니다. 서비스를 정상적으로 종료하기 위해 취소 토큰이 발생하면 ExecuteAsync 구현이 즉시 완료되어야 합니다. 그렇지 않으면 서비스가 종료 시간 제한에 종료됩니다. 자세한 내용은 IHostedService 인터페이스 섹션을 참조하세요.

자세한 내용은 BackgroundService 소스 코드를 참조하세요.

시간이 지정된 백그라운드 작업

시간이 지정된 백그라운드 작업은 System.Threading.Timer 클래스를 사용합니다. 타이머가 작업의 DoWork 메서드를 트리거합니다. 서비스 컨테이너가 Dispose에서 삭제될 때 StopAsync에서 타이머가 비활성화되고 삭제됩니다.

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는 이전에 실행된 DoWork를 마칠 때까지 기다리지 않으므로 제시된 방법이 모든 시나리오에 적합한 것은 아닐 수 있습니다. Interlocked.Increment는 여러 스레드가 동시에 executionCount를 업데이트하지 않도록 하는 원자성 작업으로 실행 카운터를 증가시키는 데 사용됩니다.

서비스는 AddHostedService 확장 메서드를 사용하여 IHostBuilder.ConfigureServices(Program.cs)에 등록됩니다.

services.AddHostedService<TimedHostedService>();

백그라운드 작업에서 범위가 지정된 서비스 사용

BackgroundService 내에서 범위가 지정된 서비스를 사용하려면 범위를 만듭니다. 기본적으로 호스팅되는 서비스에 대한 범위는 생성되지 않습니다.

범위가 지정된 백그라운드 작업 서비스에는 백그라운드 작업의 논리가 포함됩니다. 다음 예제에서

  • 서비스는 비동기입니다. DoWork 메서드는 Task를 반환합니다. 데모용으로 DoWork 메서드에서 10초의 지연이 있습니다.
  • ILogger가 서비스에 삽입됩니다.
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);
        }
    }
}

호스팅되는 서비스는 범위가 지정된 백그라운드 작업 서비스를 해결하여 DoWork 메서드를 호출하는 범위를 만듭니다. DoWorkExecuteAsync에서 대기하고 있는 Task를 반환합니다.

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

서비스는 IHostBuilder.ConfigureServices(Program.cs)에 등록됩니다. 호스팅된 서비스는 AddHostedService 확장 메서드를 사용하여 등록됩니다.

services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

대기 중인 백그라운드 작업

백그라운드 작업 큐는 .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;
    }
}

다음 QueueHostedService 예제에서

  • BackgroundProcessing 메서드는 ExecuteAsync에서 대기하고 있는 Task를 반환합니다.
  • BackgroundProcessing에서 큐의 백그라운드 작업이 큐에서 제거되고 실행됩니다.
  • 작업 항목은 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);
    }
}

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

  • IBackgroundTaskQueueMonitorLoop 서비스에 삽입됩니다.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem이 호출되어 작업 항목을 큐에 넣습니다.
  • 작업 항목은 장기 실행 백그라운드 작업을 시뮬레이션합니다.
    • 세 번의 5초 지연이 실행됩니다(Task.Delay).
    • 작업이 취소되면 try-catch 문이 OperationCanceledException을 트래핑합니다.
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);
        }
    }
}

서비스는 IHostBuilder.ConfigureServices(Program.cs)에 등록됩니다. 호스팅된 서비스는 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);
});

MonitorLoopProgram.cs에서 시작됩니다.

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

비동기 시간 제한 백그라운드 작업

다음 코드는 비동기 시간 제한 백그라운드 작업을 만듭니다.

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

작업자 서비스 템플릿은 플래그를 사용하여 .NET 네이티브 AOT(Ahead-Of-Time)--aot 를 지원합니다.

  1. 새 프로젝트를 만듭니다.
  2. 작업자 서비스 선택합니다. 다음을 선택합니다.
  3. 프로젝트 이름 필드에 프로젝트 이름을 제공하거나 기본 프로젝트 이름을 수락합니다. 다음을 선택합니다.
  4. 추가 정보 대화 상자에서 다음을 수행합니다.
  5. 프레임워크선택합니다.
  6. 네이티브 AOT 게시 검사 확인란을 선택합니다.
  7. 만들기를 실행합니다.

AOT 옵션은 프로젝트 파일에 추가합니다 <PublishAot>true</PublishAot> .


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

추가 리소스

ASP.NET Core에서 백그라운드 작업은 호스팅되는 서비스로 구현될 수 있습니다. 호스티드 서비스는 IHostedService 인터페이스를 구현하는 백그라운드 작업 논리가 있는 클래스입니다. 이 문서에서는 다음과 같이 호스티드 서비스 예를 세 가지 제공합니다.

  • 타이머에서 실행되는 백그라운드 작업.
  • 범위가 지정된 서비스를 활성화하는 호스팅되는 서비스. 범위가 지정된 서비스는 종속성 주입(DI)을 사용할 수 있습니다.
  • 순차적으로 실행되는 대기 중인 백그라운드 작업.

Worker Service 템플릿

ASP.NET Core Worker Service 템플릿은 장기간 실행되는 서비스 앱을 작성하기 위한 시작점을 제공합니다. Worker Service 템플릿에서 만든 앱은 프로젝트 파일에 작업자 SDK를 지정합니다.

<Project Sdk="Microsoft.NET.Sdk.Worker">

템플릿을 호스팅되는 서비스 앱의 기반으로 사용하려면 다음을 수행합니다.

  1. 새 프로젝트를 만듭니다.
  2. 작업자 서비스 선택합니다. 다음을 선택합니다.
  3. 프로젝트 이름 필드에 프로젝트 이름을 제공하거나 기본 프로젝트 이름을 수락합니다. 다음을 선택합니다.
  4. 추가 정보 대화 상자에서 프레임워크를 선택합니다. 만들기를 실행합니다.

Package(패키지)

Worker Service 템플릿을 기반으로 하는 앱은 Microsoft.NET.Sdk.Worker SDK를 사용하고 Microsoft.Extensions.Hosting 패키지에 대한 명시적 패키지 참조가 있습니다. 예를 들어 샘플 앱의 프로젝트 파일(BackgroundTasksSample.csproj)을 참조하세요.

Microsoft.NET.Sdk.Web SDK를 사용하는 웹앱의 경우, Microsoft.Extensions.Hosting 패키지는 공유 프레임워크에서 암시적으로 참조됩니다. 앱의 프로젝트 파일에 있는 명시적 패키지 참조는 필요하지 않습니다.

IHostedService 인터페이스

IHostedService 인터페이스는 호스트에 의해 관리되는 개체에 대한 두 가지 메서드를 정의합니다.

StartAsync

StartAsync(CancellationToken)에는 백그라운드 작업을 시작하는 논리가 포함됩니다. 다음 상황 이전StartAsync가 호출됩니다.

StartAsync는 호스트된 서비스가 순차적으로 실행되고 StartAsync 실행이 완료될 때까지 추가 서비스가 시작되지 않으므로 단기 실행 작업으로 제한되어야 합니다.

StopAsync

취소 토큰에는 종료 프로세스가 더 이상 정상이 아니어야 함을 나타내는 기본 30초 제한 시간이 있습니다. 토큰에 취소가 요청된 경우:

  • 앱이 수행하는 나머지 백그라운드 작업은 중단해야 합니다.
  • StopAsync에 호출된 모든 메서드는 즉시 반환해야 합니다.

그러나 취소가 요청된 후 호출자가 모든 작업을 완료될 때까지 작업을 취소하지 않습니다.

앱이 예기치 않게 종료된 경우(예: 앱의 프로세스가 실패한 경우), StopAsync가 호출되지 않을 수 있습니다. 따라서 메서드 호출이나 StopAsync에서 수행된 작업이 발생하지 않을 수 있습니다.

기본 30초 종료 시간 제한을 연장하려면 다음을 설정합니다.

호스팅되는 서비스는 앱 시작 시 한 번 활성화되고 앱 종료 시 정상적으로 종료됩니다. 백그라운드 작업을 실행하는 동안 오류가 발생하면 StopAsync가 호출되지 않아도 Dispose를 호출해야 합니다.

BackgroundService 기본 클래스

BackgroundService는 장기 실행 IHostedService를 구현하기 위한 기본 클래스입니다.

ExecuteAsync(CancellationToken)은 백그라운드 서비스를 실행하기 위해 호출됩니다. 구현하면 백그라운드 서비스의 전체 수명을 나타내는 Task가 반환됩니다. await를 호출하는 것처럼 ExecuteAsync가 비동기가 될 때까지 추가 서비스가 시작되지 않습니다. ExecuteAsync에서 긴 초기화 작업을 차단하는 것을 방지합니다. StopAsync(CancellationToken)의 호스트 블록은 ExecuteAsync가 완료될 때까지 대기합니다.

취소 토큰은 IHostedService.StopAsync가 호출되면 트리거됩니다. 서비스를 정상적으로 종료하기 위해 취소 토큰이 발생하면 ExecuteAsync 구현이 즉시 완료되어야 합니다. 그렇지 않으면 서비스가 종료 시간 제한에 종료됩니다. 자세한 내용은 IHostedService 인터페이스 섹션을 참조하세요.

자세한 내용은 BackgroundService 소스 코드를 참조하세요.

시간이 지정된 백그라운드 작업

시간이 지정된 백그라운드 작업은 System.Threading.Timer 클래스를 사용합니다. 타이머가 작업의 DoWork 메서드를 트리거합니다. 서비스 컨테이너가 Dispose에서 삭제될 때 StopAsync에서 타이머가 비활성화되고 삭제됩니다.

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는 이전에 실행된 DoWork를 마칠 때까지 기다리지 않으므로 제시된 방법이 모든 시나리오에 적합한 것은 아닐 수 있습니다. Interlocked.Increment는 여러 스레드가 동시에 executionCount를 업데이트하지 않도록 하는 원자성 작업으로 실행 카운터를 증가시키는 데 사용됩니다.

서비스는 AddHostedService 확장 메서드를 사용하여 IHostBuilder.ConfigureServices(Program.cs)에 등록됩니다.

services.AddHostedService<TimedHostedService>();

백그라운드 작업에서 범위가 지정된 서비스 사용

BackgroundService 내에서 범위가 지정된 서비스를 사용하려면 범위를 만듭니다. 기본적으로 호스팅되는 서비스에 대한 범위는 생성되지 않습니다.

범위가 지정된 백그라운드 작업 서비스에는 백그라운드 작업의 논리가 포함됩니다. 다음 예제에서

  • 서비스는 비동기입니다. DoWork 메서드는 Task를 반환합니다. 데모용으로 DoWork 메서드에서 10초의 지연이 있습니다.
  • ILogger가 서비스에 삽입됩니다.
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);
        }
    }
}

호스팅되는 서비스는 범위가 지정된 백그라운드 작업 서비스를 해결하여 DoWork 메서드를 호출하는 범위를 만듭니다. DoWorkExecuteAsync에서 대기하고 있는 Task를 반환합니다.

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

서비스는 IHostBuilder.ConfigureServices(Program.cs)에 등록됩니다. 호스팅된 서비스는 AddHostedService 확장 메서드를 사용하여 등록됩니다.

services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

대기 중인 백그라운드 작업

백그라운드 작업 큐는 .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;
    }
}

다음 QueueHostedService 예제에서

  • BackgroundProcessing 메서드는 ExecuteAsync에서 대기하고 있는 Task를 반환합니다.
  • BackgroundProcessing에서 큐의 백그라운드 작업이 큐에서 제거되고 실행됩니다.
  • 작업 항목은 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);
    }
}

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

  • IBackgroundTaskQueueMonitorLoop 서비스에 삽입됩니다.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem이 호출되어 작업 항목을 큐에 넣습니다.
  • 작업 항목은 장기 실행 백그라운드 작업을 시뮬레이션합니다.
    • 세 번의 5초 지연이 실행됩니다(Task.Delay).
    • 작업이 취소되면 try-catch 문이 OperationCanceledException을 트래핑합니다.
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);
        }
    }
}

서비스는 IHostBuilder.ConfigureServices(Program.cs)에 등록됩니다. 호스팅된 서비스는 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);
});

MonitorLoopProgram.cs에서 시작됩니다.

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

비동기 시간 제한 백그라운드 작업

다음 코드는 비동기 시간 제한 백그라운드 작업을 만듭니다.

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

추가 리소스

ASP.NET Core에서 백그라운드 작업은 호스팅되는 서비스로 구현될 수 있습니다. 호스티드 서비스는 IHostedService 인터페이스를 구현하는 백그라운드 작업 논리가 있는 클래스입니다. 이 문서에서는 다음과 같이 호스티드 서비스 예를 세 가지 제공합니다.

  • 타이머에서 실행되는 백그라운드 작업.
  • 범위가 지정된 서비스를 활성화하는 호스팅되는 서비스. 범위가 지정된 서비스는 종속성 주입(DI)을 사용할 수 있습니다.
  • 순차적으로 실행되는 대기 중인 백그라운드 작업.

샘플 코드 보기 및 다운로드(다운로드 방법)

Worker Service 템플릿

ASP.NET Core Worker Service 템플릿은 장기간 실행되는 서비스 앱을 작성하기 위한 시작점을 제공합니다. Worker Service 템플릿에서 만든 앱은 프로젝트 파일에 작업자 SDK를 지정합니다.

<Project Sdk="Microsoft.NET.Sdk.Worker">

템플릿을 호스팅되는 서비스 앱의 기반으로 사용하려면 다음을 수행합니다.

  1. 새 프로젝트를 만듭니다.
  2. 작업자 서비스 선택합니다. 다음을 선택합니다.
  3. 프로젝트 이름 필드에 프로젝트 이름을 제공하거나 기본 프로젝트 이름을 수락합니다. 만들기를 실행합니다.
  4. 작업자 서비스 만들기 대화 상자에서 만들기 선택합니다.

Package(패키지)

Worker Service 템플릿을 기반으로 하는 앱은 Microsoft.NET.Sdk.Worker SDK를 사용하고 Microsoft.Extensions.Hosting 패키지에 대한 명시적 패키지 참조가 있습니다. 예를 들어 샘플 앱의 프로젝트 파일(BackgroundTasksSample.csproj)을 참조하세요.

Microsoft.NET.Sdk.Web SDK를 사용하는 웹앱의 경우, Microsoft.Extensions.Hosting 패키지는 공유 프레임워크에서 암시적으로 참조됩니다. 앱의 프로젝트 파일에 있는 명시적 패키지 참조는 필요하지 않습니다.

IHostedService 인터페이스

IHostedService 인터페이스는 호스트에 의해 관리되는 개체에 대한 두 가지 메서드를 정의합니다.

StartAsync

StartAsync에는 백그라운드 작업을 시작하는 논리가 포함됩니다. 다음 상황 이전StartAsync가 호출됩니다.

앱의 파이프라인이 구성되고 ApplicationStarted가 호출된 후에 호스팅된 서비스의 StartAsync가 실행되도록 기본 동작을 변경할 수 있습니다. 기본 동작을 변경하려면 ConfigureWebHostDefaults를 호출한 후에 호스팅된 서비스(다음 예제에서 VideosWatcher)를 추가합니다.

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

취소 토큰에는 종료 프로세스가 더 이상 정상화되지 않아야 함을 나타내는 기본 5초 시간 제한이 있습니다. 토큰에 취소가 요청된 경우:

  • 앱이 수행하는 나머지 백그라운드 작업은 중단해야 합니다.
  • StopAsync에 호출된 모든 메서드는 즉시 반환해야 합니다.

그러나 취소가 요청된 후 호출자가 모든 작업을 완료될 때까지 작업을 취소하지 않습니다.

앱이 예기치 않게 종료된 경우(예: 앱의 프로세스가 실패한 경우), StopAsync가 호출되지 않을 수 있습니다. 따라서 메서드 호출이나 StopAsync에서 수행된 작업이 발생하지 않을 수 있습니다.

기본 5초 시스템 종료 시간 제한을 연장하려면 다음을 설정합니다.

호스팅되는 서비스는 앱 시작 시 한 번 활성화되고 앱 종료 시 정상적으로 종료됩니다. 백그라운드 작업을 실행하는 동안 오류가 발생하면 StopAsync가 호출되지 않아도 Dispose를 호출해야 합니다.

BackgroundService 기본 클래스

BackgroundService는 장기 실행 IHostedService를 구현하기 위한 기본 클래스입니다.

ExecuteAsync(CancellationToken)은 백그라운드 서비스를 실행하기 위해 호출됩니다. 구현하면 백그라운드 서비스의 전체 수명을 나타내는 Task가 반환됩니다. await를 호출하는 것처럼 ExecuteAsync가 비동기가 될 때까지 추가 서비스가 시작되지 않습니다. ExecuteAsync에서 긴 초기화 작업을 차단하는 것을 방지합니다. StopAsync(CancellationToken)의 호스트 블록은 ExecuteAsync가 완료될 때까지 대기합니다.

취소 토큰은 IHostedService.StopAsync가 호출되면 트리거됩니다. 서비스를 정상적으로 종료하기 위해 취소 토큰이 발생하면 ExecuteAsync 구현이 즉시 완료되어야 합니다. 그렇지 않으면 서비스가 종료 시간 제한에 종료됩니다. 자세한 내용은 IHostedService 인터페이스 섹션을 참조하세요.

StartAsync는 호스트된 서비스가 순차적으로 실행되고 StartAsync 실행이 완료될 때까지 추가 서비스가 시작되지 않으므로 단기 실행 작업으로 제한되어야 합니다. 장기 실행 작업은 ExecuteAsync에 배치해야 합니다. 자세한 내용은 BackgroundService 원본을 참조하세요.

시간이 지정된 백그라운드 작업

시간이 지정된 백그라운드 작업은 System.Threading.Timer 클래스를 사용합니다. 타이머가 작업의 DoWork 메서드를 트리거합니다. 서비스 컨테이너가 Dispose에서 삭제될 때 StopAsync에서 타이머가 비활성화되고 삭제됩니다.

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는 이전에 실행된 DoWork를 마칠 때까지 기다리지 않으므로 제시된 방법이 모든 시나리오에 적합한 것은 아닐 수 있습니다. Interlocked.Increment는 여러 스레드가 동시에 executionCount를 업데이트하지 않도록 하는 원자성 작업으로 실행 카운터를 증가시키는 데 사용됩니다.

서비스는 AddHostedService 확장 메서드를 사용하여 IHostBuilder.ConfigureServices(Program.cs)에 등록됩니다.

services.AddHostedService<TimedHostedService>();

백그라운드 작업에서 범위가 지정된 서비스 사용

BackgroundService 내에서 범위가 지정된 서비스를 사용하려면 범위를 만듭니다. 기본적으로 호스팅되는 서비스에 대한 범위는 생성되지 않습니다.

범위가 지정된 백그라운드 작업 서비스에는 백그라운드 작업의 논리가 포함됩니다. 다음 예제에서

  • 서비스는 비동기입니다. DoWork 메서드는 Task를 반환합니다. 데모용으로 DoWork 메서드에서 10초의 지연이 있습니다.
  • ILogger가 서비스에 삽입됩니다.
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);
        }
    }
}

호스팅되는 서비스는 범위가 지정된 백그라운드 작업 서비스를 해결하여 DoWork 메서드를 호출하는 범위를 만듭니다. DoWorkExecuteAsync에서 대기하고 있는 Task를 반환합니다.

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

서비스는 IHostBuilder.ConfigureServices(Program.cs)에 등록됩니다. 호스팅된 서비스는 AddHostedService 확장 메서드를 사용하여 등록됩니다.

services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

대기 중인 백그라운드 작업

백그라운드 작업 큐는 .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;
    }
}

다음 QueueHostedService 예제에서

  • BackgroundProcessing 메서드는 ExecuteAsync에서 대기하고 있는 Task를 반환합니다.
  • BackgroundProcessing에서 큐의 백그라운드 작업이 큐에서 제거되고 실행됩니다.
  • 작업 항목은 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);
    }
}

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

  • IBackgroundTaskQueueMonitorLoop 서비스에 삽입됩니다.
  • IBackgroundTaskQueue.QueueBackgroundWorkItem이 호출되어 작업 항목을 큐에 넣습니다.
  • 작업 항목은 장기 실행 백그라운드 작업을 시뮬레이션합니다.
    • 세 번의 5초 지연이 실행됩니다(Task.Delay).
    • 작업이 취소되면 try-catch 문이 OperationCanceledException을 트래핑합니다.
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);
        }
    }
}

서비스는 IHostBuilder.ConfigureServices(Program.cs)에 등록됩니다. 호스팅된 서비스는 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);
});

MonitorLoopProgram.Main에서 시작됩니다.

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

추가 리소스