キャンセル

.NET Framework Version 4 では、非同期操作または長時間にわたる同期操作に対する連携によるキャンセルのための、統一されたモデルが新たに導入されています。 このモデルは、キャンセル トークンと呼ばれる軽量のオブジェクトに基づいています。 新しいスレッドまたはタスクの作成などによってキャンセル可能な操作を呼び出すオブジェクトは、操作にトークンを渡します。 その後、その操作から他の操作にトークンのコピーを渡すことができます。 トークンを作成したオブジェクトは、操作によって実行中の処理を停止するように後で要求する場合に、そのトークンを使用して要求することができます。 キャンセル要求は、要求側のオブジェクトからのみ発行できます。各リスナーは要求を確認し、適時にその要求に応答します。 トークンのソースとそのトークンのすべてのコピーの間の関係を次の図に示します。

CancellationTokenSource と CancellationToken

新しいキャンセル モデルによって、キャンセルに対応したアプリケーションやライブラリの作成が簡単になり、次の機能がサポートされます。

  • キャンセルは連携によって行われ、リスナーに強制されません。 リスナー側でキャンセル要求に応じた適切な終了方法が決定されます。

  • 要求とリッスンが別々に行われます。 キャンセルをいつ要求するかは、キャンセル可能な操作を呼び出すオブジェクトが制御できます。

  • 要求側のオブジェクトは、1 回のメソッド呼び出しでトークンのすべてのコピーにキャンセル要求を発行できます。

  • リスナーは、複数のトークンを 1 つのリンク トークンに結合して同時にリッスンできます。

  • ライブラリ コードからのキャンセル要求をユーザー コードで確認して応答したり、ユーザー コードからのキャンセル要求をライブラリ コードで確認して応答したりすることができます。

  • リスナーはポーリング、コールバックの登録、または待機ハンドルの待機により、キャンセル要求の通知を受け取ることができます。

新しいキャンセルの型

新しいキャンセル フレームワークは、関連する一連の型として実装されます。それらの型を次の表に示します。

型名

説明

CancellationTokenSource

キャンセル トークンを作成するオブジェクトです。そのトークンのすべてのコピーに対するキャンセル要求の発行も、このオブジェクトで行います。

CancellationToken

1 つ以上のリスナーに通常はメソッド パラメーターとして渡す軽量の値型です。 リスナーはポーリング、コールバック、または待機ハンドルによって IsCancellationRequested プロパティの値を監視します。

OperationCanceledException

この例外の新しいオーバーロードは、入力パラメーターとしてキャンセル トークンを受け取ります。 リスナーは必要に応じてこの例外をスローすることで、キャンセルのソースを確認し、キャンセル要求に応答したことを通知できます。

新しいキャンセル モデルは、.NET Framework のいくつかの型に統合されています。特に重要なものは、System.Threading.Tasks.ParallelSystem.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult>、および System.Linq.ParallelEnumerable です。 新しいライブラリおよびアプリケーション コードでは、いずれもこの新しいキャンセル モデルを使用することをお勧めします。

コード例

次の例では、要求側のオブジェクトで CancellationTokenSource オブジェクトを作成し、その Token プロパティをキャンセル可能な操作に渡します。 要求を受け取る操作は、ポーリングによってトークンの IsCancellationRequested プロパティの値を監視します。 値が true になると、リスナーは適切な方法で終了できます。 この例では、メソッドの終了だけを行っています。多くの場合はこの処理だけで十分です。

メモメモ

この例では、新しいキャンセル フレームワークが従来の API と互換性があることを示すために、QueueUserWorkItem メソッドを使用しています。推奨される新しい 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;
        }
    }
}

操作のキャンセルとオブジェクトのキャンセル

新しいキャンセル フレームワークでは、キャンセルとは、オブジェクトのキャンセルではなく操作のキャンセルのことを指します。 キャンセル要求とは、必要なクリーンアップの実行後にできるだけ早く操作を停止する要求です。 1 つのキャンセル トークンで 1 つの "キャンセル可能な操作" を参照しますが、その操作はプログラムに実装されている場合があります。 トークンの 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();

オブジェクトでキャンセル可能な複数の操作の同時処理がサポートされている場合は、キャンセル可能な各操作への入力として別々のトークンを渡します。 こうすることで、他の操作には影響を与えずに 1 つの操作を取り消すことができます。

キャンセル要求のリッスンと応答

キャンセル可能な操作の実装側のユーザー デリゲートで、キャンセル要求に応じた操作の終了方法が決定されます。 多くの場合、ユーザー デリゲートで必要なクリーンアップだけを実行してすぐに制御を戻すことができます。

ただし、より複雑な場合は、キャンセルが発生したことをユーザー デリゲートでライブラリ コードに通知する必要がある場合があります。 そのような場合は、操作を適切に終了するために、デリゲートで ThrowIfCancellationRequested() を呼び出します。これによって、OperationCanceledException がスローされます。 .NET Framework Version 4 のこの例外の新しいオーバーロードは、引数として CancellationToken を受け取ります。 ライブラリ コードでは、ユーザー デリゲートのスレッドのこの例外をキャッチし、例外のトークンを調べて、例外が連携によるキャンセルを示すのかそれ以外の例外的な状況を示すのかを判断できます。

Task クラスは、この方法で OperationCanceledException を処理します。 詳細については、「タスクのキャンセル」を参照してください。

ポーリングによるリッスン

ループや再帰を伴う長時間にわたる計算では、CancellationToken.IsCancellationRequested プロパティの値を定期的にポーリングすることによってキャンセル要求をリッスンできます。 値が true の場合、メソッドはできるだけ早くクリーンアップを行って終了する必要があります。 最適なポーリング間隔はアプリケーションの種類によって異なります。 プログラムごとに、開発者が最適なポーリング間隔を決定します。 ポーリング自体がパフォーマンスに大きく影響することはありません。 ポーリングを行う方法の 1 つの例を次に示します。

    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 メソッドを使用して非同期 Web 要求を取り消す方法を次の例に示します。

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.ManualResetEventSystem.Threading.Semaphore などの同期プリミティブを待機している間、キャンセル可能な操作をブロックできる場合、CancellationToken.WaitHandle プロパティを使用してイベントとキャンセル要求の両方を待機できます。 キャンセル トークンの待機ハンドルは、キャンセル要求に応じてシグナル状態になります。メソッドは WaitAny メソッドの戻り値を使用して、そのキャンセル トークンがシグナル状態であったかどうかを判断できます。 操作はその後、必要に応じて、そのまま終了するか OperationCanceledException をスローします。

' 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 Version 4 を対象とする新しいコードでは、System.Threading.ManualResetEventSlimSystem.Threading.SemaphoreSlim はどちらも、Wait メソッドで新しいキャンセル フレームワークをサポートしています。 メソッドに CancellationToken を渡すことができ、キャンセルが要求されると、イベントが起動して OperationCanceledException がスローされます。

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);

この例より完全なコード例については、「方法: 待機ハンドルがあるキャンセル要求を待機する」を参照してください。

複数のトークンのリッスンの同時処理

リスナーでは、複数のキャンセル トークンを同時にリッスンしなければならない場合もあります。 たとえば、キャンセル可能な操作で、メソッド パラメーターの引数として外部から渡されるトークンのほかに、内部のキャンセル トークンも監視する必要がある場合などです。 これを行うには、複数のトークンを 1 つのトークンに結合できるリンク トークンのソースを作成します。次に例を示します。

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 クエリを取り消す」を参照してください。

参照

その他の技術情報

マネージ スレッド処理の基本