관리되는 스레드의 취소

.NET Framework 4부터 .NET에서는 비동기 또는 장기 실행 비동기 작업의 협조적 취소를 위한 통합 모델을 사용합니다. 이 모델은 취소 토큰이라는 경량 개체에 기반을 둡니다. 취소할 수 있는 작업 하나 이상을 호출하는 개체가 새 스레드나 작업 등을 만드는 방식으로 토큰을 각 작업에 전달합니다. 개별 작업이 토큰 복사본을 다시 다른 작업에 전달할 수 있습니다. 나중에 토큰을 만든 개체가 해당 토큰을 사용하여 관련 작업이 수행 중인 작업을 중지하도록 요청할 수 있습니다. 요청 개체만 취소 요청을 실행할 수 있고 각 수신기는 적절한 시간에 적절한 방식으로 요청을 알리고 요청에 응답해야 합니다.

협조적 취소 모델을 구현하는 일반적인 패턴은 다음과 같습니다.

  • 개별 취소 토큰을 관리하고 토큰에 취소 알림을 보내는 CancellationTokenSource 개체를 인스턴스화합니다.

  • CancellationTokenSource.Token 속성에서 반환된 토큰을 취소를 수신 대기하는 각 작업이나 스레드에 전달합니다.

  • 각 작업이나 스레드가 취소에 응답하는 메커니즘을 제공합니다.

  • CancellationTokenSource.Cancel 메서드를 호출하여 취소 알림을 제공합니다.

Important

CancellationTokenSource 클래스가 IDisposable 인터페이스를 구현합니다. 취소 토큰 소스 사용을 마치면 CancellationTokenSource.Dispose 메서드를 호출하여 토큰에 포함된 관리되지 않는 리소스를 해제해야 합니다.

다음 그림에서는 토큰 소스와 모든 토큰 복사본의 관계를 보여 줍니다.

CancellationTokenSource and cancellation tokens

협조적 취소 모델을 통해 취소 인식 애플리케이션 및 라이브러리를 더 쉽게 만들 수 있고 이 모델은 다음 기능을 지원합니다.

  • 취소는 협조적이고 수신기에 적용되지 않습니다. 수신기는 취소 요청에 대한 응답으로 정상적으로 종료하는 방법을 결정합니다.

  • 요청은 수신 대기와 다릅니다. 취소할 수 있는 작업을 호출하는 개체는 취소가 있더라도 취소가 요청되는 시점을 제어할 수 있습니다.

  • 요청 개체는 단 하나의 메서드 호출을 통해 모든 토큰 복사본에 대해 취소 요청을 실행합니다.

  • 수신기는 여러 토큰을 하나의 연결된 토큰으로 결합하여 동시에 수신 대기할 수 있습니다.

  • 사용자 코드는 라이브러리 코드에서 취소 요청을 인식하고 이 요청에 응답할 수 있고, 라이브러리 코드는 사용자 코드에서 취소 요청을 인식하고 이 요청에 응답할 수 있습니다.

  • 수신기는 대기 핸들에 대한 폴링, 콜백 등록 또는 대기를 통해 취소 요청에 대한 알림을 받을 수 있습니다.

취소 유형

취소 프레임워크는 다음 표에 나열된 관련 형식 집합으로 구현됩니다.

형식 이름 설명
CancellationTokenSource 취소 토큰을 만들고 해당 토큰의 모든 복사본에 대한 취소 요청을 실행하는 개체입니다.
CancellationToken 일반적으로 메서드 매개 변수로 수신기 하나 이상에 전달되는 경량 값 형식입니다. 수신기는 핸들을 폴링, 콜백 또는 대기하여 토큰의 IsCancellationRequested 속성 값을 모니터링합니다.
OperationCanceledException 이 예외 생성자의 오버로드는 CancellationToken을 매개 변수로 허용합니다. 수신기는 선택적으로 이 예외를 throw하여 취소의 출처를 확인하고 취소 요청에 응답했다는 것을 다른 수신기에 알립니다.

취소 모델은 여러 가지 형식으로 .NET에 통합되었습니다. 가장 중요한 형식은 System.Threading.Tasks.Parallel, System.Threading.Tasks.Task, System.Threading.Tasks.Task<TResult>System.Linq.ParallelEnumerable입니다. 모든 새 라이브러리 및 애플리케이션 코드에 대해 이 협조적 취소 모델을 사용하는 것이 좋습니다.

코드 예

다음 예제에서는 요청 개체가 CancellationTokenSource 개체를 만들고 Token 속성을 취소 가능한 작업에 전달합니다. 요청을 수신하는 작업에서는 폴링을 통해 토큰의 IsCancellationRequested 속성 값을 모니터링합니다. 값이 true가 되면 수신기가 적절한 방식이더라도 종료될 수 있습니다. 이 예제에서는 대부분 경우에 모두 필요한 메서드가 종료됩니다.

참고 항목

예제에서는 QueueUserWorkItem 메서드를 사용하여 협조적 취소 프레임워크가 레거시 API와 호환된다는 것을 보여 줍니다. 기본 System.Threading.Tasks.Task 형식을 사용하는 예제는 방법: 작업 취소 및 자식을 참조하세요.

using System;
using System.Threading;

public class Example
{
    public static void Main()
    {
        // Create the token source.
        CancellationTokenSource cts = new CancellationTokenSource();

        // Pass the token to the cancelable operation.
        ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token);
        Thread.Sleep(2500);

        // Request cancellation.
        cts.Cancel();
        Console.WriteLine("Cancellation set in token source...");
        Thread.Sleep(2500);
        // Cancellation should have happened, so call Dispose.
        cts.Dispose();
    }

    // Thread 2: The listener
    static void DoSomeWork(object? obj)
    {
        if (obj is null)
            return;

        CancellationToken token = (CancellationToken)obj;

        for (int i = 0; i < 100000; i++)
        {
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("In iteration {0}, cancellation has been requested...",
                                  i + 1);
                // Perform cleanup if necessary.
                //...
                // Terminate the operation.
                break;
            }
            // Simulate some work.
            Thread.SpinWait(500000);
        }
    }
}
// The example displays output like the following:
//       Cancellation set in token source...
//       In iteration 1430, cancellation has been requested...
Imports System.Threading

Module Example
    Public Sub Main()
        ' Create the token source.
        Dim cts As New CancellationTokenSource()

        ' Pass the token to the cancelable operation.
        ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf DoSomeWork), cts.Token)
        Thread.Sleep(2500)

        ' Request cancellation by setting a flag on the token.
        cts.Cancel()
        Console.WriteLine("Cancellation set in token source...")
        Thread.Sleep(2500)
        ' Cancellation should have happened, so call Dispose.
        cts.Dispose()
    End Sub

    ' Thread 2: The listener
    Sub DoSomeWork(ByVal obj As Object)
        Dim token As CancellationToken = CType(obj, CancellationToken)

        For i As Integer = 0 To 1000000
            If token.IsCancellationRequested Then
                Console.WriteLine("In iteration {0}, cancellation has been requested...",
                                  i + 1)
                ' Perform cleanup if necessary.
                '...
                ' Terminate the operation.
                Exit For
            End If

            ' Simulate some work.
            Thread.SpinWait(500000)
        Next
    End Sub
End Module
' The example displays output like the following:
'       Cancellation set in token source...
'       In iteration 1430, cancellation has been requested...

작업 취소 대 개체 취소

협조적 취소 프레임워크에서 취소는 개체가 아니라 작업을 나타냅니다. 취소 요청은 필요한 정리가 수행되고 나서 가능하면 즉시 작업이 중지되어야 함을 의미합니다. 취소 토큰 하나가 “취소할 수 있는 작업" 하나를 나타내야 하지만 해당 작업은 프로그램에서 구현될 수 있습니다. 토큰의 IsCancellationRequested 속성이 true로 설정되고 나면 false로 재설정될 수 없습니다. 따라서 취소 토큰이 취소된 후에는 해당 토큰을 다시 사용할 수 없습니다.

개체 취소 메커니즘이 필요하면 다음 예제와 같이 CancellationToken.Register 메서드를 호출하여 작업 취소 메커니즘을 기반으로 개체 취소 메커니즘을 적용할 수 있습니다.

using System;
using System.Threading;

class CancelableObject
{
    public string id;

    public CancelableObject(string id)
    {
        this.id = id;
    }

    public void Cancel()
    {
        Console.WriteLine("Object {0} Cancel callback", id);
        // Perform object cancellation here.
    }
}

public class Example1
{
    public static void Main()
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;

        // User defined Class with its own method for cancellation
        var obj1 = new CancelableObject("1");
        var obj2 = new CancelableObject("2");
        var obj3 = new CancelableObject("3");

        // Register the object's cancel method with the token's
        // cancellation request.
        token.Register(() => obj1.Cancel());
        token.Register(() => obj2.Cancel());
        token.Register(() => obj3.Cancel());

        // Request cancellation on the token.
        cts.Cancel();
        // Call Dispose when we're done with the CancellationTokenSource.
        cts.Dispose();
    }
}
// The example displays the following output:
//       Object 3 Cancel callback
//       Object 2 Cancel callback
//       Object 1 Cancel callback
Imports System.Threading

Class CancelableObject
    Public id As String

    Public Sub New(id As String)
        Me.id = id
    End Sub

    Public Sub Cancel()
        Console.WriteLine("Object {0} Cancel callback", id)
        ' Perform object cancellation here.
    End Sub
End Class

Module Example
    Public Sub Main()
        Dim cts As New CancellationTokenSource()
        Dim token As CancellationToken = cts.Token

        ' User defined Class with its own method for cancellation
        Dim obj1 As New CancelableObject("1")
        Dim obj2 As New CancelableObject("2")
        Dim obj3 As New CancelableObject("3")

        ' Register the object's cancel method with the token's
        ' cancellation request.
        token.Register(Sub() obj1.Cancel())
        token.Register(Sub() obj2.Cancel())
        token.Register(Sub() obj3.Cancel())

        ' Request cancellation on the token.
        cts.Cancel()
        ' Call Dispose when we're done with the CancellationTokenSource.
        cts.Dispose()
    End Sub
End Module
' The example displays output like the following:
'       Object 3 Cancel callback
'       Object 2 Cancel callback
'       Object 1 Cancel callback

개체가 동시 취소 가능 작업을 두 개 이상 지원하면 개별 토큰을 개별 취소 가능한 작업에 입력으로 전달합니다. 이런 방식으로 다른 작업에 영향을 미치지 않고 작업을 취소할 수 있습니다.

취소 요청 수신 대기 및 응답

사용자 대리자에서 취소 가능한 작업의 구현자는 취소 요청에 대한 응답으로 작업을 종료하는 방법을 결정합니다. 대부분 경우에 사용자 대리자는 필요한 정리를 수행하고 즉시 반환될 수 있습니다.

그러나 더 복잡한 경우에 사용자 대리자가 취소가 발생했다는 정보를 라이브러리 코드에 알려야 할 수 있습니다. 이 경우 작업을 종료하는 올바른 방법은 OperationCanceledException을 throw하는 ThrowIfCancellationRequested 메서드를 대리자가 호출하는 것입니다. 라이브러리 코드는 사용자 대리자 스레드에서 이 예외를 catch하고 예외의 토큰을 검사하여 예외가 협조적 취소를 나타내는지, 아니면 다른 예외적인 상황인지를 결정합니다.

Task 클래스는 이 방식으로 OperationCanceledException을 처리합니다. 자세한 내용은 Task Cancellation을 참조하세요.

폴링으로 수신 대기

루핑되거나 재귀적으로 사용되는 장기 실행 계산의 경우 CancellationToken.IsCancellationRequested 속성 값을 주기적으로 폴링하여 취소 요청을 수신 대기할 수 있습니다. 값이 true이면 메서드가 가능한 한 빠르게 정리 및 종료되어야 합니다. 최적 폴링 빈도는 애플리케이션 형식에 따라 다릅니다. 특정 프로그램에 대한 최적 폴링 빈도는 개발자가 결정할 수 있습니다. 폴링 자체는 성능에 큰 영향을 미치지 않습니다. 다음 예제에서는 한 가지 가능한 폴링 방법을 보여 줍니다.

static void NestedLoops(Rectangle rect, CancellationToken token)
{
   for (int col = 0; col < rect.columns && !token.IsCancellationRequested; col++) {
      // Assume that we know that the inner loop is very fast.
      // Therefore, polling once per column in the outer loop condition
      // is sufficient.
      for (int row = 0; row < rect.rows; row++) {
         // Simulating work.
         Thread.SpinWait(5_000);
         Console.Write("{0},{1} ", col, row);
      }
   }

   if (token.IsCancellationRequested) {
      // Cleanup or undo here if necessary...
      Console.WriteLine("\r\nOperation canceled");
      Console.WriteLine("Press any key to exit.");

      // If using Task:
      // token.ThrowIfCancellationRequested();
   }
}
Shared Sub NestedLoops(ByVal rect As Rectangle, ByVal token As CancellationToken)
    Dim col As Integer
    For col = 0 To rect.columns - 1
        ' Assume that we know that the inner loop is very fast.
        ' Therefore, polling once per column in the outer loop condition
        ' is sufficient.
        For col As Integer = 0 To rect.rows - 1
            ' Simulating work.
            Thread.SpinWait(5000)
            Console.Write("0',1' ", x, y)
        Next
    Next

    If token.IsCancellationRequested = True Then
        ' Cleanup or undo here if necessary...
        Console.WriteLine(vbCrLf + "Operation canceled")
        Console.WriteLine("Press any key to exit.")

        ' If using Task:
        ' token.ThrowIfCancellationRequested()
    End If
End Sub

더 자세한 예제를 보려면 방법: 폴링을 통해 취소 요청 수신 대기를 참조하세요.

콜백을 등록하여 수신 대기

일부 작업은 적절한 시기에 취소 토큰의 값을 확인할 수 없는 방식으로 차단될 수 있습니다. 이러한 경우에는 취소 요청이 수신될 때 메서드를 차단 해제하는 콜백 메서드를 등록할 수 있습니다.

Register 메서드는 특히 이 목적으로 사용되는 CancellationTokenRegistration 개체를 반환합니다. 다음 예제에서는 Register 메서드를 사용하여 비동기 웹 요청을 취소하는 방법을 보여 줍니다.

using System;
using System.Net;
using System.Threading;

class Example4
{
    static void Main()
    {
        CancellationTokenSource cts = new CancellationTokenSource();

        StartWebRequest(cts.Token);

        // cancellation will cause the web
        // request to be cancelled
        cts.Cancel();
    }

    static void StartWebRequest(CancellationToken token)
    {
        WebClient wc = new WebClient();
        wc.DownloadStringCompleted += (s, e) => Console.WriteLine("Request completed.");

        // Cancellation on the token will
        // call CancelAsync on the WebClient.
        token.Register(() =>
        {
            wc.CancelAsync();
            Console.WriteLine("Request cancelled!");
        });

        Console.WriteLine("Starting request.");
        wc.DownloadStringAsync(new Uri("http://www.contoso.com"));
    }
}
Imports System.Net
Imports System.Threading

Class Example
    Private Shared Sub Main()
        Dim cts As New CancellationTokenSource()

        StartWebRequest(cts.Token)

        ' cancellation will cause the web 
        ' request to be cancelled
        cts.Cancel()
    End Sub

    Private Shared Sub StartWebRequest(token As CancellationToken)
        Dim wc As New WebClient()
        wc.DownloadStringCompleted += Function(s, e) Console.WriteLine("Request completed.")

        ' Cancellation on the token will 
        ' call CancelAsync on the WebClient.
        token.Register(Function()
                           wc.CancelAsync()
                           Console.WriteLine("Request cancelled!")

                       End Function)

        Console.WriteLine("Starting request.")
        wc.DownloadStringAsync(New Uri("http://www.contoso.com"))
    End Sub
End Class

CancellationTokenRegistration 개체는 스레드 동기화를 관리하고 콜백이 정확한 시점에 실행을 중지하는지 확인합니다.

시스템 응답성을 확인하고 교착 상태를 방지하려면 콜백을 등록할 때 다음 지침을 따라야 합니다.

  • 콜백 메서드는 동기적으로 호출되어 콜백이 반환될 때까지 Cancel에 대한 호출이 반환되지 않으므로 콜백 메서드는 빨라야 합니다.

  • 콜백이 실행되는 동안 Dispose를 호출하고 콜백이 대기하도록 잠금을 유지하면 프로그램에서 교착 상태가 발생할 수 있습니다. Dispose가 반환되고 나서 콜백에 필요한 리소스를 해제할 수 있습니다.

  • 콜백은 한 콜백에서 수동 스레드 또는 SynchronizationContext 사용을 수행하면 안 됩니다. 콜백이 특정 스레드에서 실행되어야 하면 대상 syncContext가 활성 SynchronizationContext.Current가 되도록 지정할 수 있는 System.Threading.CancellationTokenRegistration 생성자를 사용합니다. 콜백에서 수동 스레딩을 수행하면 교착 상태가 발생할 수 있습니다.

더 자세한 예제를 보려면 방법: 취소 요청에 대한 콜백 등록을 참조하세요.

대기 핸들을 사용하여 수신 대기

취소할 수 있는 작업이 System.Threading.ManualResetEvent 또는 System.Threading.Semaphore와 같은 동기화 기본 형식에서 대기하는 동안 차단되면 CancellationToken.WaitHandle 속성을 사용하여 작업이 이벤트 및 취소 요청에서 대기하도록 할 수 있습니다. 취소 토큰의 대기 핸들은 취소 요청에 대한 응답으로 신호가 전송되고 메서드는 WaitAny 메서드의 반환 값을 사용하여 신호를 전송한 취소 토큰이었는지를 판별할 수 있습니다. 그러면 작업이 종료되거나 적절하게 OperationCanceledException을(를) throw할 수 있습니다.

// Wait on the event if it is not signaled.
int eventThatSignaledIndex =
       WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
                          new TimeSpan(0, 0, 20));
' Wait on the event if it is not signaled.
Dim waitHandles() As WaitHandle = {mre, token.WaitHandle}
Dim eventThatSignaledIndex =
    WaitHandle.WaitAny(waitHandles, _
                       New TimeSpan(0, 0, 20))

System.Threading.ManualResetEventSlimSystem.Threading.SemaphoreSlimWait 메서드에서 취소 프레임워크를 지원합니다. CancellationToken을 메서드에 전달하고 취소가 요청될 대 이벤트가 활성화되어 OperationCanceledException을 throw합니다.

try
{
    // mres is a ManualResetEventSlim
    mres.Wait(token);
}
catch (OperationCanceledException)
{
    // Throw immediately to be responsive. The
    // alternative is to do one more item of work,
    // and throw on next iteration, because
    // IsCancellationRequested will be true.
    Console.WriteLine("The wait operation was canceled.");
    throw;
}

Console.Write("Working...");
// Simulating work.
Thread.SpinWait(500000);
Try
    ' mres is a ManualResetEventSlim
    mres.Wait(token)
Catch e As OperationCanceledException
    ' Throw immediately to be responsive. The
    ' alternative is to do one more item of work,
    ' and throw on next iteration, because
    ' IsCancellationRequested will be true.
    Console.WriteLine("Canceled while waiting.")
    Throw
End Try

' Simulating work.
Console.Write("Working...")
Thread.SpinWait(500000)

더 자세한 예제를 보려면 방법: 대기 핸들이 있는 취소 요청 수신 대기를 참조하세요.

동시에 여러 토큰 수신 대기

경우에 따라 수신기는 여러 취소 토큰을 동시에 수신 대기해야 할 수 있습니다. 예를 들어 취소 가능한 작업은 외부에서 메서드 매개 변수에 인수로 전달되는 토큰 이외에 내부 취소 토큰을 모니터링해야 할 수 있습니다. 이 작업을 하려면 다음 예제와 같이 토큰 두 개 이상을 토큰 하나로 결합할 수 있는 연결된 토큰 소스를 만듭니다.

public void DoWork(CancellationToken externalToken)
{
    // Create a new token that combines the internal and external tokens.
    this.internalToken = internalTokenSource.Token;
    this.externalToken = externalToken;

    using (CancellationTokenSource linkedCts =
            CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken))
    {
        try
        {
            DoWorkInternal(linkedCts.Token);
        }
        catch (OperationCanceledException)
        {
            if (internalToken.IsCancellationRequested)
            {
                Console.WriteLine("Operation timed out.");
            }
            else if (externalToken.IsCancellationRequested)
            {
                Console.WriteLine("Cancelling per user request.");
                externalToken.ThrowIfCancellationRequested();
            }
        }
    }
}
Public Sub DoWork(ByVal externalToken As CancellationToken)
    ' Create a new token that combines the internal and external tokens.
    Dim internalToken As CancellationToken = internalTokenSource.Token
    Dim linkedCts As CancellationTokenSource =
    CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken)
    Using (linkedCts)
        Try
            DoWorkInternal(linkedCts.Token)
        Catch e As OperationCanceledException
            If e.CancellationToken = internalToken Then
                Console.WriteLine("Operation timed out.")
            ElseIf e.CancellationToken = externalToken Then
                Console.WriteLine("Canceled by external token.")
                externalToken.ThrowIfCancellationRequested()
            End If
        End Try
    End Using
End Sub

작업을 완료했을 때 연결된 토큰 소스에서 Dispose를 호출해야 합니다. 더 자세한 예제를 보려면 방법: 여러 취소 요청 수신 대기를 참조하세요.

라이브러리 코드와 사용자 코드 간 협력

통합 취소 프레임워크를 사용하면 라이브러리 코드에서 사용자 코드를 취소하고 사용자 코드에서 협조적 방식으로 라이브러리 코드를 취소할 수 있습니다. 원활한 협력은 다음 지침에 따라 양측에 의존합니다.

  • 취소 가능한 작업을 제공하는 라이브러리 코드는 사용자 코드가 취소를 요청할 수 있도록 외부 취소 토큰을 허용하는 공용 메서드도 제공해야 합니다.

  • 라이브러리 코드가 사용자 코드를 호출하면 라이브러리 코드는 OperationCanceledException(externalToken)을 협조적 취소로 해석하고 오류 예외로 해석할 필요는 없습니다.

  • 사용자 대리자는 적절한 시간에 라이브러리 코드의 취소 요청에 응답해야 합니다.

System.Threading.Tasks.TaskSystem.Linq.ParallelEnumerable은 이들 지침을 따르는 클래스의 예입니다. 자세한 내용은 작업 취소 방법: PLINQ 쿼리 취소를 참조하세요.

참고 항목