🔥 CancellationToken, CancellationTokenSource
https://learn.microsoft.com/ko-kr/dotnet/api/system.threading.cancellationtokensource?view=net-9.0
✅ 개념 정리
CancellationToken과 CancellationTokenSource는 C#에서 비동기 작업(Task)이나 스레드를 안전하게 취소할 수 있도록 제공되는 기능합니다.
이를 활용하면 비동기 작업을 중단하거나, 긴 루프를 중지할 수 있도록 관리할 수 있습니다.
🔹 CancellationTokenSource
- 취소 요청을 생성하고 관리하는 역할을 합니다.
CancellationToken을 생성할 수 있으며,Cancel()메서드를 호출하면 해당 토큰을 통해 작업을 중단할 수 있습니다.
🔹 CancellationToken
CancellationTokenSource에서 생성한 취소 토큰을 비동기 작업이나 스레드에 전달하여, 취소 여부를 확인하는 데 사용됩니다.
| 역할 | 설명 |
|---|---|
CancellationTokenSource | 취소 요청을 보내는 쪽 (발신자) 역할을 합니다. Cancel(), CancelAfter(), Dispose() 등을 호출 할 수 있습니다. |
CancellationToken | 취소 요청을 받는 쪽 (수신자) 역할입니다. IsCancellationRequested로 확인하거나, ThrowIfCancellationRequested() 등을 사용해 취소 여부를 감지할 수 있습니다. |
✅ 기본적인 사용법
🔹 CancellationToken 사용 예제
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Task task = DoWorkAsync(token);
// 3초 후에 작업 취소 요청
await Task.Delay(3000);
cts.Cancel();
await task;
}
static async Task DoWorkAsync(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
// 작업이 취소되었는지 확인
if (token.IsCancellationRequested)
{
Console.WriteLine("작업이 취소되었습니다.");
return;
}
Console.WriteLine($"작업 진행 중... {i}");
await Task.Delay(1000); // 1초 대기
}
}
}
작업 진행 중... 0 작업 진행 중... 1 작업 진행 중... 2 작업이 취소되었습니다.
🔍 설명
-
CancellationTokenSource를 생성하여CancellationToken을 가져옴 -
DoWorkAsync()메서드에서token.IsCancellationRequested를 통해 취소 여부 확인 -
Task.Delay(3000)후cts.Cancel()을 호출하여 비동기 작업을 취소
✅ 알아두면 유용한 기능
🔹 ThrowIfCancellationRequested()
이전의 예제에서는 IsCancellationRequested로 확인 후 return 했지만, ThrowIfCancellationRequested()를 사용하면 예외(Exception) 형태로 중단할 수도 있습니다.
static async Task DoWorkAsync(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested(); // 취소 요청 시 예외 발생
Console.WriteLine($"작업 진행 중... {i}");
await Task.Delay(1000);
}
}
예외 처리 추가 (try-catch로 OperationCanceledException을 catch가 가능)
try
{
await DoWorkAsync(token);
}
catch (OperationCanceledException)
{
Console.WriteLine("작업이 취소되었습니다. (예외 발생)");
}
🔹 Task.Run()에서 CancellationToken
비동기 작업을 Task.Run()을 사용하여 실행하는 경우, CancellationToken을 직접 전달할 수 있습니다.
Task.Run()과 함께 사용할 경우, CancellationToken을 직접 전달하면 자동으로 OperationCanceledException이 발생하여 처리하기 편리합니다.
CancellationTokenSource cts = new CancellationTokenSource();
Task task = Task.Run(() =>
{
for (int i = 0; i < 10; i++)
{
cts.Token.ThrowIfCancellationRequested();
Console.WriteLine($"작업 진행 중... {i}");
Thread.Sleep(1000);
}
}, cts.Token); // CancellationToken을 직접 전달
await Task.Delay(3000);
cts.Cancel(); // 3초 후 취소
🔹 Register()를 활용한 CallBack 함수
CancellationToken.Register()를 사용하면, 취소될 때 특정 작업을 실행할 수 있습니다.
cts.Token.Register(() => Console.WriteLine("취소 요청이 감지되었습니다!"));
사용 예제
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// 취소 요청 시 실행될 콜백 등록
token.Register(() => Console.WriteLine("취소 요청이 감지되었습니다!"));
Task task = DoWorkAsync(token);
await Task.Delay(3000);
cts.Cancel(); // 3초 후 취소 요청
await task;
}
static async Task DoWorkAsync(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
if (token.IsCancellationRequested)
{
Console.WriteLine("작업이 취소되었습니다.");
return;
}
Console.WriteLine($"작업 진행 중... {i}");
await Task.Delay(1000);
}
}
}
작업 진행 중... 0 작업 진행 중... 1 작업 진행 중... 2 취소 요청이 감지되었습니다! 작업이 취소되었습니다.
🔹 async와 CancellationToken을 활용한 HttpClient 요청 취소
HttpClient에서 CancellationToken을 사용하여 HTTP 요청을 중단할 수도 있습니다.
HttpClient 요청이 너무 오래 걸리면 CancellationToken을 이용해 요청을 취소할 수 있습니다.
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using (HttpClient client = new HttpClient())
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(2))) // 2초 후 자동 취소
{
try
{
HttpResponseMessage response = await client.GetAsync("https://example.com", cts.Token);
Console.WriteLine("응답 수신 완료!");
}
catch (TaskCanceledException)
{
Console.WriteLine("HTTP 요청이 취소되었습니다.");
}
}
}
}
🔹 CancelAfter()를 활용한 자동 취소
일정 시간이 지나면 자동으로 취소되도록 설정 가능 (CancelAfter(milliseconds))
cts.Token.Register(() => Console.WriteLine("취소 요청이 감지되었습니다!"));
사용 예제 (5초 후 자동 취소되는 코드)
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000); // 5초 후 자동 취소
try
{
await DoWorkAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("작업이 시간 초과로 취소되었습니다.");
}
}
static async Task DoWorkAsync(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();
Console.WriteLine($"작업 진행 중... {i}");
await Task.Delay(1000);
}
}
}
🔹 LinkedTokenSource를 사용하여 여러 토큰을 결합
여러 CancellationTokenSource를 결합하여 하나의 토큰으로 관리 가능
어느 하나라도 취소되면 전체 작업이 취소됨
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
CancellationTokenSource cts1 = new CancellationTokenSource();
CancellationTokenSource cts2 = new CancellationTokenSource();
// 두 개의 토큰을 하나로 결합
using (CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token))
{
Task task = DoWorkAsync(linkedCts.Token);
await Task.Delay(3000);
cts1.Cancel(); // 하나의 토큰만 취소해도 작업 전체가 중단됨
await task;
}
}
static async Task DoWorkAsync(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();
Console.WriteLine($"작업 진행 중... {i}");
await Task.Delay(1000);
}
}
}
결과적으로 cts1.Cancel(); 호출 시 linkedCts도 취소됨
🔹 Parallel.ForEach()에서 CancellationToken 적용
Parallel.ForEach()에서 CancellationToken을 사용하여 병렬 처리 도중 특정 조건에서 중단할 수도 있습니다.
ParallelOptions을 통해 취소 토큰을 설정하면 병렬 처리에서도 취소를 손쉽게 관리할 수 있습니다.
ParallelOptions options = new ParallelOptions
{
CancellationToken = cts.Token,
MaxDegreeOfParallelism = 4 // 최대 4개의 스레드 사용
};
try
{
Parallel.ForEach(Enumerable.Range(1, 100), options, (num, state) =>
{
options.CancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"Processing {num}");
Thread.Sleep(500); // 가상의 작업
});
}
catch (OperationCanceledException)
{
Console.WriteLine("병렬 작업이 취소되었습니다.");
}
✅ 주의 사항
🔹 리소스 정리의 중요성
취소된 작업에서도 적절한 리소스 정리가 필요합니다.
try/finally 블록이나 using 문을 적절이 활용하는 것이 좋습니다.
static async Task ProcessFileAsync(string path, CancellationToken token)
{
FileStream file = null;
try
{
file = new FileStream(path, FileMode.Open);
// 파일 처리 작업
token.ThrowIfCancellationRequested();
}
catch (OperationCanceledException)
{
Console.WriteLine("파일 처리가 취소되었습니다.");
throw;
}
finally
{
// 취소 여부와 상관없이 항상 실행됨
file?.Dispose();
}
}
🔹 CancellationToken 확인 빈도
장시간 실행되는 작업에서는 적절한 간격으로 취소 토큰을 확인하는 것이 중요합니다.
취소 토큰 확인은 가벼운 연산이지만, 매우 빈번하게 호출되는 코드에서는 성능에 영향을 줄 수 있습니다.
static void ProcessLargeData(IEnumerable<int> items, CancellationToken token)
{
int count = 0;
foreach (var item in items)
{
// 100개 항목마다 취소 여부 확인 (너무 자주 확인하면 성능 저하)
if (count++ % 100 == 0 && token.IsCancellationRequested)
{
Console.WriteLine("작업이 취소되었습니다.");
return;
}
// 항목 처리...
}
}
🔹 CancellationTokenSource.Dispose() 중요성
CancellationTokenSource는 사용 후 수명이 끝나면 반드시 Dispose()를 호출해야 합니다. (메모리 누수)
using 문을 사용하는 것을 추천합니다.
using (var cts = new CancellationTokenSource())
{
// 작업 수행
}
// 여기서 자동으로 cts.Dispose() 호출됨
✅ 공식 문서 사용 예제
// https://learn.microsoft.com/ko-kr/dotnet/api/system.threading.cancellationtokensource?view=net-9.0
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
public class Example
{
public static void Main()
{
// Define the cancellation token.
CancellationTokenSource source = new CancellationTokenSource();
CancellationToken token = source.Token;
Random rnd = new Random();
Object lockObj = new Object();
List<Task<int[]>> tasks = new List<Task<int[]>>();
TaskFactory factory = new TaskFactory(token);
for (int taskCtr = 0; taskCtr <= 10; taskCtr++) {
int iteration = taskCtr + 1;
tasks.Add(factory.StartNew( () => {
int value;
int[] values = new int[10];
for (int ctr = 1; ctr <= 10; ctr++) {
lock (lockObj) {
value = rnd.Next(0,101);
}
if (value == 0) {
source.Cancel();
Console.WriteLine("Cancelling at task {0}", iteration);
break;
}
values[ctr-1] = value;
}
return values;
}, token));
}
try {
Task<double> fTask = factory.ContinueWhenAll(tasks.ToArray(),
(results) => {
Console.WriteLine("Calculating overall mean...");
long sum = 0;
int n = 0;
foreach (var t in results) {
foreach (var r in t.Result) {
sum += r;
n++;
}
}
return sum/(double) n;
} , token);
Console.WriteLine("The mean is {0}.", fTask.Result);
}
catch (AggregateException ae) {
foreach (Exception e in ae.InnerExceptions) {
if (e is TaskCanceledException)
Console.WriteLine("Unable to compute mean: {0}",
((TaskCanceledException) e).Message);
else
Console.WriteLine("Exception: " + e.GetType().Name);
}
}
finally {
source.Dispose();
}
}
}
// Repeated execution of the example produces output like the following:
// Cancelling at task 5
// Unable to compute mean: A task was canceled.
//
// Cancelling at task 10
// Unable to compute mean: A task was canceled.
//
// Calculating overall mean...
// The mean is 5.29545454545455.
//
// Cancelling at task 4
// Unable to compute mean: A task was canceled.
//
// Cancelling at task 5
// Unable to compute mean: A task was canceled.
//
// Cancelling at task 6
// Unable to compute mean: A task was canceled.
//
// Calculating overall mean...
// The mean is 4.97363636363636.
//
// Cancelling at task 4
// Unable to compute mean: A task was canceled.
//
// Cancelling at task 5
// Unable to compute mean: A task was canceled.
//
// Cancelling at task 4
// Unable to compute mean: A task was canceled.
//
// Calculating overall mean...
// The mean is 4.86545454545455.



