다음을 통해 공유


관리되는 스레드의 취소

.NET Framework 4부터 .NET은 비동기 또는 장기 실행 동기 작업의 협조적 취소를 위해 통합 모델을 사용합니다. 이 모델은 취소 토큰이라는 경량 개체를 기반으로 합니다. 예를 들어 새 스레드 또는 태스크를 만들어 취소할 수 있는 작업을 하나 이상 호출하는 개체는 각 작업에 토큰을 전달합니다. 개별 작업은 토큰의 복사본을 다른 작업에 전달할 수 있습니다. 일정한 시점에서 토큰을 만든 개체는 작업을 멈추도록 요청할 수 있습니다. 요청하는 개체만 취소 요청을 실행할 수 있으며 각 수신기는 요청을 확인하고 적절하고 시기 적절한 방식으로 응답할 책임이 있습니다.

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

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

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

  • 각 태스크 또는 스레드가 취소에 응답할 수 있는 메커니즘을 제공합니다.

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

중요합니다

CancellationTokenSource 클래스가 IDisposable 인터페이스를 구현합니다. 취소 토큰 원본 사용을 마쳤으면 CancellationTokenSource.Dispose 메서드를 호출하여 보유하는 관리되지 않는 리소스를 해제해야 합니다.

다음 그림에서는 토큰 원본과 해당 토큰의 모든 복사본 간의 관계를 보여 줍니다.

CancellationTokenSource 및 취소 토큰

협조적 취소 모델을 사용하면 취소 인식 애플리케이션 및 라이브러리를 더 쉽게 만들 수 있으며 다음 기능을 지원합니다.

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

  • 요청은 수신 대기와 다릅니다. 취소 가능한 작업을 호출하는 개체는 취소가 요청된 경우 제어할 수 있습니다.

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

  • 여러 토큰을 하나의 연결된 토큰으로 결합하여 청취자가 동시에 여러 토큰을 들을 수 있습니다.

  • 사용자 코드는 라이브러리 코드의 취소 요청을 확인하고 응답할 수 있으며 라이브러리 코드는 사용자 코드의 취소 요청을 확인하고 응답할 수 있습니다.

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

취소 유형

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

형식 이름 설명
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 Example1
    Public Sub Main1()
        ' 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 {id} Cancel callback");
        // 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 ExampleOb1
    Public Sub MainOb1()
        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

개체가 둘 이상의 동시 취소 가능 작업을 지원하는 경우 별도의 토큰을 각 취소 가능한 작업에 입력으로 전달합니다. 이렇게 하면 다른 작업에 영향을 주지 않고 한 작업을 취소할 수 있습니다.

취소 요청 수신 대기 및 응답

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

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

Task 클래스는 이러한 방식으로 OperationCanceledException 처리합니다. 자세한 내용은 작업 취소참조하세요.

폴링으로 수신 대기

반복하거나 재귀하는 장기 실행 계산의 경우 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 row As Integer = 0 To rect.rows - 1
            ' Simulating work.
            Thread.SpinWait(5000)
            Console.Write("0',1' ", col, row)
        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.Http;
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)
    {
        var client = new HttpClient();

        token.Register(() =>
        {
            client.CancelPendingRequests();
            Console.WriteLine("Request cancelled!");
        });

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

Class Example4
    Private Shared Sub Main4()
        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 client As New HttpClient()

        token.Register(Sub()
                           client.CancelPendingRequests()
                           Console.WriteLine("Request cancelled!")
                       End Sub)

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

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

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

  • 콜백 메서드는 동기적으로 호출되며, 따라서 콜백이 반환될 때까지 Cancel 호출이 반환되지 않으므로 빠르게 실행되어야 합니다.

  • 콜백이 실행되는 동안 Dispose 호출하고 콜백이 대기 중인 잠금을 보유하면 프로그램이 교착 상태에 빠질 수 있습니다. Dispose 반환되면 콜백에 필요한 모든 리소스를 해제할 수 있습니다.

  • 콜백에서는 수동으로 스레드를 사용하거나 SynchronizationContext을 직접 사용해서는 안 됩니다. 특정 스레드에서 콜백을 실행해야 하는 경우, 대상 syncContext가 활성 상태에서 System.Threading.CancellationTokenRegistration로 지정하도록 할 수 있는 SynchronizationContext.Current 생성자를 사용하십시오. 콜백에서 수동 스레딩을 수행하면 교착 상태가 발생할 수 있습니다.

자세한 예제는 방법: 취소 요청콜백 등록을 참조하세요.

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

취소 가능한 작업이 System.Threading.ManualResetEvent 또는 System.Threading.Semaphore과 같은 동기화 기본 형식에서 대기하면서 차단될 수 있는 경우, CancellationToken.WaitHandle 속성을 사용하여 작업이 이벤트 및 취소 요청을 모두 대기하도록 할 수 있습니다. 취소 요청에 대한 응답으로 취소 토큰의 대기 핸들이 신호를 받게 되며, 메서드는 WaitAny 메서드의 반환 값을 사용하여 신호를 받은 취소 토큰인지 여부를 확인할 수 있습니다. 그런 다음 작업을 종료하거나, 적절한 경우에는 OperationCanceledException예외를 던질 수 있습니다.

// 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.SemaphoreSlim 모두 Wait 메서드에서 취소 프레임워크를 지원합니다. 메서드에 CancellationToken을 전달할 수 있으며, 취소가 요청되면 이벤트가 깨어나고 OperationCanceledException이 발생합니다.

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 쿼리취소를 참조하세요.

참고하십시오