次の方法で共有


マネージド スレッドでの取り消し

.NET Framework 4 以降、.NET では、非同期操作または実行時間の長い同期操作を協調的に取り消す統合モデルが使用されます。 このモデルは、キャンセル トークンと呼ばれる軽量オブジェクトに基づいています。 1 つ以上の取り消し可能な操作を呼び出すオブジェクト (たとえば、新しいスレッドやタスクを作成するなど) は、各操作にトークンを渡します。 個々の操作で、トークンのコピーを他の操作に渡すことができます。 後でトークンを作成したオブジェクトは、トークンを使用して、操作が実行していることを停止するように要求できます。 取り消し要求を発行できるのは、要求元のオブジェクトだけです。各リスナーは、要求に気付き、適切かつタイムリーに応答する責任を負います。

協調キャンセル モデルを実装するための一般的なパターンは次のとおりです。

  • 個々のキャンセル トークンにキャンセル通知を管理して送信する CancellationTokenSource オブジェクトをインスタンス化します。

  • 取り消しをリッスンする各タスクまたはスレッドに、 CancellationTokenSource.Token プロパティによって返されるトークンを渡します。

  • 各タスクまたはスレッドが取り消しに応答するためのメカニズムを提供します。

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

Von Bedeutung

CancellationTokenSource クラスは IDisposable インターフェイスを実装しています。 取り消しトークン ソースの使用が完了したら、 CancellationTokenSource.Dispose メソッドを呼び出して、保持しているアンマネージ リソースを解放する必要があります。

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

CancellationTokenSource とキャンセル トークン

協調キャンセル モデルを使用すると、取り消し対応のアプリケーションとライブラリを簡単に作成でき、次の機能がサポートされます。

  • 取り消しは協調的であり、リスナーに対して強制されません。 リスナーは、取り消し要求に応答して正常に終了する方法を決定します。

  • 要求はリッスンとは異なります。 取り消し可能な操作を呼び出すオブジェクトは、取り消しが要求されるタイミング (ある場合) を制御できます。

  • 要求オブジェクトは、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になると、リスナーは適切な方法で終了できます。 この例では、メソッドは終了するだけです。これは、多くの場合に必要なすべてです。

この例では、 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...

操作の取り消しとオブジェクトの取り消し

協調キャンセル フレームワークでは、取り消しはオブジェクトではなく操作を指します。 取り消し要求は、必要なクリーンアップが実行された後、できるだけ早く操作を停止する必要があることを意味します。 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 {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

オブジェクトで複数の同時キャンセル可能な操作がサポートされている場合は、個別のキャンセル可能な各操作に個別のトークンを入力として渡します。 そうすることで、他の操作に影響を与えることなく、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 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 メソッドを使用して非同期 Web 要求を取り消す方法を示しています。

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 がアクティブなSynchronizationContext.Currentであることを指定できるSystem.Threading.CancellationTokenRegistration コンストラクターを使用します。 コールバックで手動スレッド処理を実行すると、デッドロックが発生する可能性があります。

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

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

取り消し可能な操作が、 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)

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

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

場合によっては、リスナーが複数のキャンセル トークンを同時にリッスンする必要があります。 たとえば、取り消し可能な操作では、メソッド パラメーターに引数として外部から渡されたトークンに加えて、内部キャンセル トークンを監視する必要がある場合があります。 これを実現するには、次の例に示すように、2 つ以上のトークンを 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.TaskSystem.Linq.ParallelEnumerable は、これらのガイドラインに従うクラスの例です。 詳細については、「タスクの 取り消 し」および「 方法: PLINQ クエリを取り消す」を参照してください。

こちらも参照ください