본문 바로가기

C#

C# CancellationToken으로 작업 취소 구현하기

CancellationToken은 비동기/병렬 작업을 ‘협조적’으로 취소하기 위한 표준 도구입니다. 토큰을 메서드에 전달하고, 해당 메서드가 수시로 토큰을 확인해 스스로 중단하도록 설계합니다.

1. 기본 사용법

CancellationTokenSource로 토큰을 만들고 메서드에 전달합니다. 작업 내에서는 ThrowIfCancellationRequested 또는 IsCancellationRequested를 사용해 주기적으로 확인합니다.

using System;
using System.Threading;
using System.Threading.Tasks;

static async Task DoWorkAsync(CancellationToken token)
{
    for (int i = 0; i < 10; i++)
    {
        token.ThrowIfCancellationRequested(); // 협조적 취소 지점
        Console.WriteLine($"Step {i}");
        await Task.Delay(300, token); // 토큰을 지원하는 API에 반드시 전달
    }
}

static async Task Main()
{
    using var cts = new CancellationTokenSource();

    var task = DoWorkAsync(cts.Token);
    cts.CancelAfter(500); // 0.5초 후 자동 취소

    try
    {
        await task;
    }
    catch (OperationCanceledException) when (cts.IsCancellationRequested)
    {
        Console.WriteLine("작업이 취소되었습니다.");
    }
}

2. I/O 작업 취소 (예: HttpClient)

대부분의 비동기 I/O API는 CancellationToken을 지원합니다. 반드시 토큰을 전달해야 즉시 취소가 반영됩니다.

using var client = new HttpClient();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); // 3초 타임아웃

try
{
    using var response = await client.GetAsync("https://example.com", cts.Token);
    response.EnsureSuccessStatusCode();
    string body = await response.Content.ReadAsStringAsync(cts.Token);
    Console.WriteLine(body);
}
catch (OperationCanceledException)
{
    Console.WriteLine(cts.IsCancellationRequested ? "사용자/타임아웃으로 취소" : "기타 취소");
}

3. 타임아웃과 연결 토큰

사용자 취소와 타임아웃을 함께 쓰려면 CreateLinkedTokenSource로 토큰을 연결합니다.

using var userCts = new CancellationTokenSource();
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
    userCts.Token, timeoutCts.Token);

await DoWorkAsync(linkedCts.Token);

// 어느 한쪽이 취소되면 linkedCts.Token도 취소됩니다.

4. API 설계와 모범 사례

- 공개 메서드는 CancellationToken 매개변수를 제공하고 기본값을 허용합니다.

public Task<int> CalculateAsync(Input input, CancellationToken cancellationToken = default)
{
    cancellationToken.ThrowIfCancellationRequested();
    return InnerCalculateAsync(input, cancellationToken);
}

- 루프/긴 연산에 주기적으로 토큰을 확인합니다. CPU 바운드라면 N회마다 확인하는 식으로 비용을 조절합니다.

for (int i = 0; i < items.Length; i++)
{
    if ((i & 0xFF) == 0) cancellationToken.ThrowIfCancellationRequested();
    Process(items[i]);
}

- 토큰을 지원하는 API(Delay, WaitAsync, Stream.ReadAsync, EF Core, HttpClient 등)에는 항상 토큰을 전달합니다.

- OperationCanceledException은 정상적인 흐름입니다. 로깅 시 오류로 취급하지 말고, 필요하면 어떤 원인(사용자/타임아웃)인지 판단해 메시지를 분기합니다.

- 토큰이 필요 없을 때는 CancellationToken.None을 사용합니다.

5. 비동기 스트림 취소

IAsyncEnumerable을 사용할 때도 토큰을 전달해 열거를 중단할 수 있습니다.

await foreach (var item in GetItemsAsync().WithCancellation(token))
{
    Console.WriteLine(item);
}

6. 디버깅/주의사항

- 취소는 ‘협조적’입니다. 토큰을 무시하면 취소가 안 됩니다.

- Task.Run에 토큰을 전달해도 작업 시작 전 취소만 보장합니다. 작업 내부에서도 토큰을 사용해야 합니다.

- 타임아웃으로 인한 취소도 대개 OperationCanceledException(TaskCanceledException)을 발생시키므로, 연결된 CancellationTokenSource 상태로 구분하는 것이 실용적입니다.

- CancellationTokenSource는 IDisposable입니다. using을 권장합니다.

핵심은 “토큰을 받으면, 토큰을 전파하고, 주기적으로 확인”입니다. 이 패턴만 지키면 C#에서 예측 가능하고 빠르게 취소되는 비동기 코드를 쉽게 구현할 수 있습니다.