マネージド スレッドのキャンセル

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

連携によるキャンセル処理モデルを実装するための一般的なパターンは次のとおりです。

  • CancellationTokenSource オブジェクトのインスタンスを作成します。このオブジェクトでは、個々のキャンセル トークンへのキャンセル通知を管理し、送信します。

  • CancellationTokenSource.Token プロパティによって返されるトークンを、キャンセルをリッスンしているそれぞれのタスクまたはスレッドに渡します。

  • それぞれのタスクまたはスレッドに対し、キャンセルに応答するメカニズムを提供します。

  • キャンセルの通知を提供する CancellationTokenSource.Cancel メソッドを呼び出します。

重要

CancellationTokenSource クラスは、IDisposable インターフェイスを実装します。 キャンセル トークン ソースの使用を終えた後は、必ず CancellationTokenSource.Dispose メソッドを呼び出して、キャンセル トークン ソースが保持しているアンマネージ リソースを解放する必要があります。

トークンのソースとそのトークンのすべてのコピーの間の関係を次の図に示します。

CancellationTokenSource and cancellation tokens

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

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

  • 要求とリッスンが区別して行われます。 キャンセルをいつ要求するか (もし要求する場合) は、キャンセル可能な操作を呼び出すオブジェクトの側で制御できます。

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

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

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

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

キャンセルの型

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

型名 説明
CancellationTokenSource キャンセル トークンを作成するオブジェクトです。そのトークンのすべてのコピーに対するキャンセル要求の発行も、このオブジェクトで行います。
CancellationToken 1 つ以上のリスナーに渡される軽量な値型で、通常はメソッド パラメーターとして渡されます。 リスナーはポーリング、コールバック、または待機ハンドルによってトークンの IsCancellationRequested プロパティの値を監視します。
OperationCanceledException この例外のコンストラクターのオーバーロードで、CancellationToken をパラメーターとして受け入れます。 リスナーは必要に応じてこの例外をスローすることができます。この例外により、キャンセルのソースを確認し、キャンセル要求にそのリスナーが応答したことを他のリスナーに通知します。

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

コード例

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

注意

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

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

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

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

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

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

ただし、より複雑なケースでは、キャンセルが発生したことをユーザー デリゲートからライブラリ コードに通知する必要がある場合があります。 そのような場合、操作を終了するための正しい方法はユーザー デリゲートから ThrowIfCancellationRequested メソッドを呼び出すことです。これにより、OperationCanceledException がスローされます。 ライブラリ コードでは、ユーザー デリゲートのスレッドでこの例外をキャッチし、例外のトークンを調べて、この例外が連携によるキャンセルを示すのか、それ以外の例外的な状況を示すのかを判断できます。

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

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

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

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

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 を使用したりしないでください。 特定のスレッドでコールバックを実行する必要がある場合は、System.Threading.CancellationTokenRegistration コンストラクターを使用します。これにより、ターゲットの syncContext がアクティブな SynchronizationContext.Current であることを指定できます。 コールバックで手動によるスレッド処理を実行すると、デッドロックが発生する可能性があります。

より完全なコード例については、「方法:キャンセル要求のコールバックを登録する」を参照してください。

待機ハンドルを使用したリッスン

System.Threading.ManualResetEventSystem.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)

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

同時に複数のトークンをリッスンする

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

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.Task および System.Linq.ParallelEnumerable は、これらのガイドラインに従ったクラスの例です。 詳細については、「タスクのキャンセル」と「方法:PLINQ クエリを取り消す」をご覧ください。

関連項目