🔥 CancellationToken, CancellationTokenSource
✅ 개념 정리
CancellationToken
과 CancellationTokenSource
는 C#에서 비동기 작업(Task)이나 스레드를 안전하게 취소할 수 있도록 제공되는 기능합니다.
이를 활용하면 비동기 작업을 중단하거나, 긴 루프를 중지할 수 있도록 관리할 수 있습니다.
🔹 CancellationTokenSource
- 취소 요청을 생성하고 관리하는 역할을 합니다.
CancellationToken
을 생성할 수 있으며,Cancel()
메서드를 호출하면 해당 토큰을 통해 작업을 중단할 수 있습니다.
🔹 CancellationToken
CancellationTokenSource
에서 생성한 취소 토큰을 비동기 작업이나 스레드에 전달하여, 취소 여부를 확인하는 데 사용됩니다.
✅ 기본적인 사용법
🔹 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() 호출됨