C#의 CancellationToken, CancellationTokenSource

🔥 CancellationToken, CancellationTokenSource

✅ 개념 정리

CancellationTokenCancellationTokenSource는 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
작업이 취소되었습니다.
🔍 설명
  1. CancellationTokenSource를 생성하여 CancellationToken을 가져옴
  2. DoWorkAsync() 메서드에서 token.IsCancellationRequested를 통해 취소 여부 확인
  3. 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() 호출됨

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

위로 스크롤