Quick Reference
CancellationTokenSource는 취소 요청을 만들고, CancellationToken은 그 요청을 작업에 전달합니다. token은 작업을 강제로 죽이지 않으므로, 취소를 지원하는 하위 API에 넘기거나 직접 확인해야 합니다. 사용자 취소와 시간 제한을 함께 다룰 때는 token을 연결하고 원인을 따로 판별합니다.
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
try
{
await DoWorkAsync(timeoutCts.Token);
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
Console.WriteLine("시간 제한으로 취소되었습니다.");
}
async Task DoWorkAsync(CancellationToken ct)
{
for (int i = 0; i < 10; i++)
{
ct.ThrowIfCancellationRequested(); // 취소 신호 확인
await Task.Delay(500, ct); // Delay도 취소 인식
Console.WriteLine($"Step {i}");
}
}문법
어떤 취소 패턴을 먼저 고르면 되나
| 상황 | 먼저 떠올릴 것 |
|---|---|
| 수동 취소 신호 발행 | CancellationTokenSource.Cancel() |
| 시간 제한 | CancelAfter(...) |
| 여러 취소 신호 결합 | CreateLinkedTokenSource(...) |
| 즉시 중단 | ThrowIfCancellationRequested() |
| 정리 후 종료 | IsCancellationRequested + finally |
협력 취소 모델 — 왜 token을 체인 전체에 전달해야 하는가
CancellationToken은 강제 종료가 아니라 협력(cooperative) 취소 모델입니다. 취소 신호가 발행되더라도 메서드나 라이브러리가 신호를 확인하고 반응해야 중단됩니다. 취소를 지원하는 하위 API에는 token을 전달하고, 의도적으로 취소할 수 없는 작업이라면 그 경계를 호출자에게 드러냅니다.
// ❌ 중간에서 token을 무시하면 취소 불가
async Task ProcessOrderAsync(int orderId, CancellationToken ct)
{
var order = await _repo.GetOrderAsync(orderId, ct); // ct 전달 ✅
var result = await _pricing.CalculateAsync(order); // ct 누락 ❌ — 이 작업은 계속될 수 있음
await _notifier.SendAsync(result, ct); // 이후 단계만 취소를 인식
}
// ✅ 모든 단계에 ct 전달
async Task ProcessOrderAsync(int orderId, CancellationToken ct)
{
var order = await _repo.GetOrderAsync(orderId, ct);
var result = await _pricing.CalculateAsync(order, ct);
await _notifier.SendAsync(result, ct);
}CancelAfter 와 timeout — 시간 기반 취소 패턴
API 호출, 데이터베이스 쿼리, 외부 서비스 연동처럼 응답 시간이 보장되지 않는 작업에는 timeout을 강제해야 합니다. CancellationTokenSource의 CancelAfter를 사용하면 지정한 시간이 지난 뒤 자동으로 취소 신호가 발행됩니다. HttpClient.Timeout처럼 별도 속성으로 timeout을 제공하지 않는 라이브러리에서 특히 유용합니다.
// 방법 1: 생성자에서 timeout 지정
using var cts1 = new CancellationTokenSource(TimeSpan.FromSeconds(10));
// 방법 2: 생성 후 CancelAfter 호출
using var cts2 = new CancellationTokenSource();
cts2.CancelAfter(TimeSpan.FromSeconds(10));
// 방법 3: CreateLinkedTokenSource와 CancelAfter 조합 (아래 참고)
var result = await _httpClient.GetStringAsync(url, cts1.Token);한 CTS가 취소되기 전에는 CancelAfter를 다시 호출해 시간을 다시 셀 수 있습니다. 이미 취소된 CTS는 취소 상태로 남으므로, 다음 요청의 timeout 용도로 되살아나지 않습니다.
LinkedTokenSource — 여러 취소 신호 결합
실제 서비스에서는 취소 신호가 여러 곳에서 옵니다. 사용자가 요청을 취소하는 신호(상위 token), 요청별 timeout 신호(로컬 CTS), 서버 종료 신호(host lifetime token). CancellationTokenSource.CreateLinkedTokenSource는 이 신호들을 하나로 묶어 어느 하나라도 취소되면 연결된 token이 함께 취소됩니다.
// ASP.NET Core 컨트롤러 — 요청 취소 + 자체 timeout 결합
[HttpGet("{id}")]
public async Task<IActionResult> GetAsync(int id, CancellationToken requestCt)
{
// requestCt: 클라이언트가 연결을 끊으면 자동 취소
// 추가로 5초 timeout 적용
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
requestCt, timeoutCts.Token);
try
{
var data = await _service.FetchAsync(id, linkedCts.Token);
return Ok(data);
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
return Problem(statusCode: StatusCodes.Status504GatewayTimeout);
}
catch (OperationCanceledException) when (requestCt.IsCancellationRequested)
{
// 클라이언트가 더 이상 응답을 기다리지 않는다. 서버 오류로 기록하지 않는다.
return new EmptyResult();
}
}모든 OperationCanceledException을 499로 바꾸지 않습니다. 499는 표준 HTTP 상태 코드가 아니며, timeout·클라이언트 연결 종료·호스트 종료는 운영 정책과 로그 수준이 다릅니다. 위 예시는 timeout만 504로 응답하고, 이미 끊긴 요청에는 본문을 쓰지 않는 한 가지 정책입니다.
ThrowIfCancellationRequested vs IsCancellationRequested — 선택 기준
취소 신호를 확인하는 두 가지 방법은 서로 다른 상황에 맞습니다. ThrowIfCancellationRequested()는 취소 시 OperationCanceledException을 던져 즉시 실행을 중단합니다. 대부분의 경우 이 방식이 적합합니다. IsCancellationRequested는 bool을 반환하므로 취소 시 정리(cleanup) 작업을 수행해야 하거나, 예외 없이 루프를 빠져나가야 할 때 사용합니다.
// ThrowIfCancellationRequested — 즉시 중단, 예외로 전파 (권장)
async Task ProcessItemsAsync(IEnumerable<Item> items, CancellationToken ct)
{
foreach (var item in items)
{
ct.ThrowIfCancellationRequested(); // 취소 시 즉시 예외
await ProcessAsync(item, ct);
}
}
// IsCancellationRequested — 취소 시 정리 후 종료
async Task ProcessWithCleanupAsync(CancellationToken ct)
{
Resource? resource = null;
try
{
resource = await AcquireResourceAsync(ct);
while (!ct.IsCancellationRequested) // 취소될 때까지 반복
{
await DoWorkAsync(resource, ct);
}
}
finally
{
resource?.Dispose(); // 취소 여부 상관없이 정리
}
}// ❌ 토큰은 받았지만 하위 비동기 API에 전달하지 않음
await Task.Delay(5000);
// ✅ 취소 가능한 API에는 토큰도 함께 전달
await Task.Delay(5000, ct);취소 API 선택
| 상황 | 적합한 선택 |
|---|---|
| 수동 취소 신호 발행 | cts.Cancel() |
| 시간 기반 자동 취소 | cts.CancelAfter(timeout) 또는 생성자 파라미터 |
| 여러 취소 신호 결합 | CancellationTokenSource.CreateLinkedTokenSource(...) |
| 취소 시 즉시 예외로 중단 | ct.ThrowIfCancellationRequested() |
| 취소 확인 후 정리 작업 수행 | ct.IsCancellationRequested + finally |
| ASP.NET Core 요청 취소 | 컨트롤러 파라미터 CancellationToken 자동 주입 |
주의할 점
CancellationTokenSource는 IDisposable을 구현합니다. 작업이 끝나고 다른 작업이 더 이상 CTS를 쓰지 않을 때 using으로 정리합니다. Dispose()는 취소 요청을 보내지 않으며, disposed CTS의 CancelAfter 같은 멤버 호출은 ObjectDisposedException이 됩니다.
// ❌ 취소된 CTS는 다음 작업의 timeout 용도로 재사용할 수 없다
var cts = new CancellationTokenSource();
cts.Cancel();
cts.CancelAfter(5000); // 이미 취소되어 새 취소 시간을 예약하지 않음
// ✅ 작업마다 새 CTS를 만들고 스코프 끝에서 Dispose
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(5));
await DoWorkAsync(cts.Token);
// 다음 작업에는 새 인스턴스
using var cts2 = new CancellationTokenSource();
await DoWorkAsync(cts2.Token);TryReset()은 .NET 6+에 있지만, 아직 취소되지 않았고 단독 소유자가 완료된 작업의 등록을 관리할 수 있을 때만 성공합니다. 일반 요청 코드에서는 재사용보다 새 CTS가 더 명확합니다. OperationCanceledException은 catch (Exception)에도 잡히므로, 취소와 실제 오류를 구분할 catch를 먼저 둡니다.
참고 링크
2 sources