취소
.NET Framework 버전 4에는 비동기 또는 장기 실행 동기 작업의 협조적 취소를 위한 통합 모델이 새로 도입되었습니다. 이 모델은 취소 토큰이라고 하는 간단한 개체를 기반으로 합니다. 새 스레드 또는 작업을 만드는 등의 방법으로 취소할 수 있는 작업을 호출하는 개체는 해당 작업에 취소 토큰을 전달합니다. 그러면 토큰을 받은 작업은 다시 이 토큰의 복사본을 다른 작업에 전달할 수 있습니다. 토큰을 만든 개체는 나중에 이 토큰을 사용하여 실행 중인 작업의 중지를 요청할 수 있습니다. 취소 요청은 요청 개체만 실행할 수 있으며 각 수신기는 이러한 요청을 인식하고 적시에 이에 응답해야 합니다. 다음 그림에서는 토큰 소스와 이 토큰의 모든 복사본 간 관계를 보여 줍니다.
새 취소 모델을 사용하면 취소 인식 응용 프로그램과 라이브러리를 보다 쉽게 만들고 다음과 같은 기능을 지원할 수 있습니다.
취소가 협조적으로 수행되며 수신기에 강제로 적용되지 않습니다. 수신기는 취소 요청에 대한 응답으로 작업을 적절하게 종료하는 방법을 결정합니다.
요청이 수신과 다릅니다. 취소할 수 있는 작업을 호출하는 개체는 취소가 요청되는 시점(있는 경우)을 제어할 수 있습니다.
요청 개체가 하나의 메서드 호출만 사용하여 토큰의 모든 복사본에 대한 취소 요청을 실행합니다.
수신기에서 여러 토큰을 하나의 연결된 토큰으로 조인하여 동시에 수신할 수 있습니다.
사용자 코드는 라이브러리 코드의 취소 요청을 인식하고 이에 응답할 수 있으며 라이브러리 코드는 사용자 코드의 취소 요청을 인식하고 이에 응답할 수 있습니다.
수신기가 폴링, 콜백 등록 또는 대기 핸들 대기를 통해 취소 요청 알림을 받을 수 있습니다.
새 취소 형식
새 취소 프레임워크는 다음 표에 나와 있는 관련 형식의 집합으로 구현됩니다.
형식 이름 |
설명 |
---|---|
취소 토큰을 만들 뿐 아니라 이 토큰의 모든 복사본에 대한 취소 요청을 실행하는 개체입니다. |
|
하나 이상의 수신기에 대개 메서드 매개 변수로 전달되는 간단한 값 형식입니다. 수신기는 폴링, 콜백 또는 대기 핸들을 통해 토큰의 IsCancellationRequested 속성 값을 모니터링합니다. |
|
이 예외의 새 오버로드는 취소 토큰을 입력 매개 변수로 받아들입니다. 수신기는 취소의 소스를 확인하고 다른 수신기에 취소 요청에 응답했음을 알리기 위해 선택적으로 이 예외를 throw할 수 있습니다. |
새 취소 모델은 여러 가지 형식으로 .NET Framework에 통합되었습니다. 가장 중요한 형식은 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 형식을 사용하는 예제는 방법: 작업 및 해당 자식 취소를 참조하십시오. |
Shared Sub CancelWithThreadPoolMiniSnippet()
'Thread 1: The Requestor
' Create the token source.
Dim cts As New CancellationTokenSource()
' Pass the token to the cancelable operation.
ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf DoSomeWork), cts.Token)
' Request cancellation by setting a flag on the token.
cts.Cancel()
' end block
End Sub
'Thread 2: The Listener
Shared Sub DoSomeWork(ByVal obj As Object)
Dim token As CancellationToken = CType(obj, CancellationToken)
For i As Integer = 0 To 1000000
' Simulating work.
Thread.SpinWait(5000000)
If token.IsCancellationRequested Then
' Perform cleanup if necessary.
'...
' Terminate the operation.
Exit For
End If
Next
End Sub
static void CancelWithThreadPoolMiniSnippet()
{
//Thread 1: The Requestor
// Create the token source.
CancellationTokenSource cts = new CancellationTokenSource();
// Pass the token to the cancelable operation.
ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token);
// Request cancellation by setting a flag on the token.
cts.Cancel();
}
//Thread 2: The Listener
static void DoSomeWork(object obj)
{
CancellationToken token = (CancellationToken)obj;
for (int i = 0; i < 100000; i++)
{
// Simulating work.
Thread.SpinWait(5000000);
if (token.IsCancellationRequested)
{
// Perform cleanup if necessary.
//...
// Terminate the operation.
break;
}
}
}
작업 취소 및 개체 취소
새로운 취소 프레임워크에서 취소는 개체가 아니라 작업에 적용됩니다. 취소 요청은 필요한 모든 정리가 수행된 후 가능한 한 빨리 작업이 중지되어야 함을 의미합니다. 하나의 취소 토큰은 하나의 "취소할 수 있는 작업"에 해당하지만, 프로그램에서 이 작업이 구현될 수 있습니다. 토큰의 IsCancellationRequested 속성을 true로 설정한 후에는 false로 다시 설정할 수 없습니다. 따라서 취소 토큰은 취소된 후 다시 사용할 수 없습니다.
개체 취소 메커니즘이 필요하면 다음 예제와 같이 작업 취소 메커니즘을 기반으로 할 수 있습니다.
Dim cts As New CancellationTokenSource()
Dim token As CancellationToken = cts.Token
' User defined Class with its own method for cancellation
Dim obj1 As New MyCancelableObject()
Dim obj2 As New MyCancelableObject()
Dim obj3 As New MyCancelableObject()
' 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()
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// User defined Class with its own method for cancellation
var obj1 = new MyCancelableObject();
var obj2 = new MyCancelableObject();
var obj3 = new MyCancelableObject();
// 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();
개체가 둘 이상의 취소할 수 있는 작업을 지원하면 각 작업에 개별 토큰을 입력으로 전달합니다. 이렇게 하면 다른 작업에 영향을 주지 않고 특정 작업을 취소할 수 있습니다.
취소 요청 수신 및 응답
취소할 수 있는 작업의 구현자는 사용자 대리자에서 취소 요청의 응답으로 작업을 종료하는 방법을 결정합니다. 대부분의 경우 사용자 대리자는 필요한 모든 정리를 수행한 후 즉시 반환할 수 있습니다.
그러나 보다 복잡한 경우에는 취소가 발생했음을 사용자 대리자가 라이브러리 코드에 알려야 할 수 있습니다. 이러한 경우 작업을 종료하는 올바른 방법은 대리자가 ThrowIfCancellationRequested()를 호출하여 OperationCanceledException이 throw되도록 하는 것입니다. .NET Framework 버전 4에서는 이 예외의 새 오버로드가 CancellationToken을 인수로 받아들입니다. 라이브러리 코드는 사용자 대리자 스레드에서 이 예외를 catch하고 이 예외의 토큰을 검사하여 이 예외가 협조적 취소를 나타내는지 아니면 다른 예외 상황을 나타내는지 여부를 확인할 수 있습니다.
Task 클래스는 이런 식으로 OperationCanceledException을 처리합니다. 자세한 내용은 작업 취소를 참조하십시오.
폴링으로 수신
반복되는 장기 실행 계산의 경우 CancellationToken.IsCancellationRequested 속성의 값을 주기적으로 폴링하여 취소 요청을 수신 대기할 수 있습니다. 값이 true이면 메서드가 정리를 수행한 후 가능한 한 빨리 종료해야 합니다. 최적의 폴링 빈도는 응용 프로그램의 종료에 따라 달라집니다. 지정된 프로그램에 대한 최적의 폴링 빈도는 개발자가 결정합니다. 폴링 자체는 성능에 큰 영향을 주지 않습니다. 다음 예제에서는 폴링할 수 있는 한 가지 방법을 보여 줍니다.
Shared Sub NestedLoops(ByVal rect As Rectangle, ByVal token As CancellationToken)
For x As Integer = 0 To rect.columns
For y As Integer = 0 To rect.rows
' Simulating work.
Thread.SpinWait(5000)
Console.Write("0' end block,1' end block ", x, y)
Next
' Assume that we know that the inner loop is very fast.
' Therefore, checking once per row is sufficient.
If token.IsCancellationRequested = True Then
' Cleanup or undo here if necessary...
Console.WriteLine("\r\nCancelling after row 0' end block.", x)
Console.WriteLine("Press any key to exit.")
' then...
Exit For
' ...or, if using Task:
' token.ThrowIfCancellationRequested()
End If
Next
End Sub
static void NestedLoops(Rectangle rect, CancellationToken token)
{
for (int x = 0; x < rect.columns && !token.IsCancellationRequested; x++)
{
for (int y = 0; y < rect.rows; y++)
{
// Simulating work.
Thread.SpinWait(5000);
Console.Write("{0},{1} ", x, y);
}
// Assume that we know that the inner loop is very fast.
// Therefore, checking once per row is sufficient.
if (token.IsCancellationRequested)
{
// Cleanup or undo here if necessary...
Console.WriteLine("\r\nCancelling after row {0}.", x);
Console.WriteLine("Press any key to exit.");
// then...
break;
// ...or, if using Task:
// token.ThrowIfCancellationRequested();
}
}
}
자세한 예제는 방법: 폴링을 통해 취소 요청 수신 대기를 참조하십시오.
콜백을 등록하여 수신
일부 작업은 취소 토큰의 값을 적시에 확인할 수 없는 방식으로 차단될 수 있습니다. 이러한 경우 취소 요청을 받을 때 메서의 차단을 해제하는 콜백 메서드를 등록할 수 있습니다.
Register 메서드는 특별히 이러한 용도로 사용되는 CancellationTokenRegistration 개체를 반환합니다. 다음 예제에서는 Register 메서드를 사용하여 비동기 웹 요청을 취소하는 방법을 보여 줍니다.
Dim cts As New CancellationTokenSource()
Dim token As CancellationToken = cts.Token
Dim wc As New WebClient()
' To request cancellation on the token
' will call CancelAsync on the WebClient.
token.Register(Sub() wc.CancelAsync())
Console.WriteLine("Starting request")
wc.DownloadStringAsync(New Uri("https://www.contoso.com"))
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
WebClient wc = new WebClient();
// To request cancellation on the token
// will call CancelAsync on the WebClient.
token.Register(() => wc.CancelAsync());
Console.WriteLine("Starting request");
wc.DownloadStringAsync(new Uri("https://www.contoso.com"));
CancellationTokenRegistration 개체는 스레드 동기화를 관리하고 정확한 시점에 콜백이 실행을 중지하도록 합니다.
교착 상태가 발생하지 않고 시스템이 제대로 응답하게 하려면 콜백을 등록할 때 다음 지침을 따라야 합니다.
콜백 메서드는 동기적으로 호출되므로 콜백이 반환될 때까지 Cancel에 대한 호출이 반환되지 않습니다. 따라서 콜백 메서드는 속도가 빨라야 합니다.
콜백이 실행되는 동안 Dispose를 호출하면 콜백이 기다리고 있는 잠금을 보내지 않게 되므로 프로그램이 교착 상태에 빠질 수 있습니다. Dispose가 반환되고 나면 콜백에 필요한 리소스를 모두 해제할 수 있습니다.
콜백에서 수동 스레드를 수행하거나 콜백에 SynchronizationContext를 사용하지 말아야 합니다. 특정 스레드에 대해 콜백을 실행해야 하는 경우에는 System.Threading.CancellationTokenRegistration 생성자를 사용하여 대상 syncContext가 활성 SynchronizationContext.Current임을 지정할 수 있도록 해야 합니다. 콜백에서 수동 스레드를 수행하면 교착 상태가 발생할 수 있습니다.
자세한 예제는 방법: 취소 요청에 대한 콜백 등록를 참조하십시오.
대기 핸들을 사용하여 수신
취소할 수 있는 작업이 System.Threading.ManualResetEvent 또는 System.Threading.Semaphore 같은 동기화 기본 형식을 기다리는 동안 차단될 수 있는 경우 CancellationToken.WaitHandle 속성을 사용하여 작업이 이벤트와 취소 요청을 둘 다 기다리도록 설정할 수 있습니다. 취소 토큰의 대기 핸들은 취소 요청에 대한 응답으로 신호를 받으면 메서드는 WaitAny 메서드의 반환 값을 사용하여 이 취소 토큰이 신호를 받은 취소 토큰인지 여부를 확인할 수 있습니다. 그러면 작업이 필요에 따라 종료되거나 OperationCanceledException을 throw할 수 있습니다.
' Wait on the event if it is not signaled.
Dim myWaitHandle(2) As WaitHandle
myWaitHandle(0) = mre
myWaitHandle(1) = token.WaitHandle
Dim eventThatSignaledIndex =
WaitHandle.WaitAny(myWaitHandle, _
New TimeSpan(0, 0, 20))
// Wait on the event if it is not signaled.
int eventThatSignaledIndex =
WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
new TimeSpan(0, 0, 20));
.NET Framework 버전 4, System.Threading.ManualResetEventSlim 및 System.Threading.SemaphoreSlim을 대상으로 하는 새 코드에서는 둘 다 해당 Wait 메서드의 새 취소 프레임워크를 지원합니다. 이 메서드에 CancellationToken을 전달하여 취소가 요청될 때 이벤트가 깨어나서 OperationCanceledException을 throw하도록 할 수 있습니다.
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)
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);
자세한 예제는 방법: 대기 핸들이 있는 취소 요청 수신 대기를 참조하십시오.
동시에 여러 토큰 수신
경우에 따라 수신기에서 여러 취소 토큰을 동시에 수신할 수 있습니다. 예를 들어, 취소할 수 있는 작업은 외부에서 메서드 매개 변수의 인수로 전달되는 토큰 외에도 내부 취소 토큰을 모니터링해야 할 수 있습니다. 이를 위해서는 다음 예제와 같이 둘 이상의 토큰을 하나의 토큰으로 조인할 수 있는 연결된 소스 토큰을 만듭니다.
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
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();
}
}
}
}
연결된 토큰 소스를 사용하여 조인 작업을 완료한 후에는 해당 연결된 토큰 소스에 대해 Dispose를 호출해야 합니다. 자세한 예제는 방법: 여러 개의 취소 요청 수신 대기를 참조하십시오.
라이브러리 코드와 사용자 코드 간의 협조
통합 취소 프레임워크를 사용하면 협조적 방식으로 라이브러리 코드로 사용자 코드를 취소하고 사용자 코드로 라이브러리 코드를 취소할 수 있습니다. 원활한 협조가 이루어지려면 두 코드가 다음과 같은 지침을 따라야 합니다.
라이브러리 코드가 취소할 수 있는 작업을 제공하는 경우 라이브러리 코드는 사용자 코드도 취소를 요청할 수 있도록 외부 취소 토큰을 받아들이는 공용 메서드도 제공해야 합니다.
라이브러리 코드가 사용자 코드를 호출하는 경우 라이브러리 코드는 OperationCanceledException(externalToken)을 협조적 취소로 해석해야 하며, 실패 예외로 해석할 필요는 없습니다.
사용자 대리자는 라이브러리 코드의 취소 요청에 적시에 응답하려고 시도해야 합니다.
System.Threading.Tasks.Task 및 System.Linq.ParallelEnumerable은 이러한 지침을 따르는 클래스의 예제입니다. 자세한 내용은 작업 취소 및 방법: PLINQ 쿼리 취소를 참조하십시오.