Отмена в управляемых потоках

Начиная с .NET Framework 4, в .NET введена новая универсальная модель совместной отмены асинхронных или долго выполняющихся синхронных операций. Эта модель построена на простом объекте, называемом токеном отмены. Объект, который вызывает одну или несколько отменяемых операций, например, путем создания новых потоков или задач, передает этот токен в каждую операцию. Операция, в свою очередь, передает копии этого токена в другие операции. Некоторое время спустя объект, создавший токен, может использовать его для запроса остановки выполнения операции. Запрос на отмену может создавать только запрашивающий объект, и каждый прослушиватель должен обнаружить этот запрос, чтобы правильно и своевременно отреагировать на него.

Общая схема реализации модели совместной отмены выглядит следующим образом:

  • Создается экземпляр объекта CancellationTokenSource, который управляет уведомлениями об отмене и передает их отдельным токенам отмены.

  • В каждую задачу или поток, ожидающий отмены, передается токен, возвращенный свойством CancellationTokenSource.Token.

  • Каждой задачи или каждому потоку предоставляется механизм реагирования на отмену.

  • Вызывается метод CancellationTokenSource.Cancel для предоставления уведомления об отмене.

Внимание

Класс CancellationTokenSource реализует интерфейс IDisposable. По завершении использования источника токена отмены обязательно вызовите метод CancellationTokenSource.Dispose, чтобы освободить все занятые неуправляемые ресурсы.

На рисунке ниже показана связь между источником токена и всеми копиями токена.

CancellationTokenSource and cancellation tokens

Модель совместной отмены упрощает создание приложений и библиотек, поддерживающих отмену. Она также поддерживает перечисленные ниже возможности.

  • Отмена является совместной и не осуществляется принудительно на прослушивателе. Прослушиватель сам определяет порядок корректного завершения в ответ на запрос отмены.

  • Запрос осуществляется отдельно от прослушивания. Объект, который вызывает отменяемую операцию, может управлять временем создания запроса отмены (а также самим фактом создания подобного запроса).

  • Запрашивающий объект создает запрос на отмену для всех копий токена, используя только один вызов метода.

  • Прослушиватель может одновременно ожидать несколько маркеров, объединив их в один связанный маркер.

  • Пользовательский код может отслеживать запросы на отмену из кода библиотеки и реагировать на них, а код библиотеки, в свою очередь, может отслеживать запросы на отмену из пользовательского кода и реагировать на них.

  • Для уведомления прослушивателей о запросах на отмену может использоваться опрос, регистрация обратных вызовов или ожидание дескрипторов ожидания.

Типы отмены

Инфраструктура отмены реализована в виде набора связанных типов, приведенных в таблице ниже.

Введите имя Description
CancellationTokenSource Объект, который создает токен отмены и запрос на отмену для всех копий этого токена.
CancellationToken Простой тип значения, передаваемый одному или нескольким прослушивателям, обычно в виде параметра метода. Прослушиватели отслеживают значение свойства IsCancellationRequested токена посредством опроса, обратного вызова или дескриптора ожидания.
OperationCanceledException Перегрузки конструктора этого исключения принимают CancellationToken в качестве параметра. Прослушиватели могут также создавать это исключение для проверки источника отмены и уведомления остальных прослушивателей об ответе на запрос отмены.

Модель отмены интегрирована в несколько типов .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 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...

Отмена операции и отмена объекта

В рамках инфраструктуры совместной отмены осуществляется отмена операций, а не объектов. Запрос на отмену означает, что операция должна быть остановлена как можно скорее после выполнения всех необходимых очисток. Один токен отмены должен относиться к одной отменяемой операции, однако эта операция может быть реализована в программе. После того как свойство 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

Если объект поддерживает несколько параллельных отменяемых операций, в каждую отменяемую операцию следует передавать отдельный токен. Это позволяет отменить одну операцию, не затрагивая при этом остальные.

Прослушивание запросов на отмену и ответ на них

Объект, реализующий отменяемую операцию, в пользовательском делегате определяет способ завершения операции в ответ на запрос отмены. Во многих случаях пользовательский делегат может выполнить необходимую очистку, а затем немедленный возврат.

Однако в более сложных случаях может потребоваться, чтобы пользовательский делегат уведомлял код библиотеки об отмене. В таких случаях, чтобы правильно завершить операцию, следует вызвать из делегата метод ThrowIfCancellationRequested, который создает исключение OperationCanceledException. Код библиотеки может перехватить это исключение в потоке пользовательского делегата и проверить токен исключения, чтобы определить, указывает ли исключение на совместную отмену или возникновение другой исключительной ситуации.

Класс Task обрабатывает OperationCanceledException таким образом. Для получения дополнительной информации см. Task Cancellation.

Прослушивание с помощью опросов

Для длительных циклических или рекурсивных вычислений можно прослушивать запрос на отмену путем периодического опроса значения свойства 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 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 для отмены асинхронного веб-запроса.

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, который позволяет задать активный объект SynchronizationContext.Current в качестве целевого объекта syncContext. Выполнение ручного потока в обратном вызове может привести к взаимоблокировке.

Более полный пример см. в статье Практическое руководство. Регистрация обратных вызовов для запросов на отмену.

Прослушивание с помощью дескриптора ожидания

В случаях, когда отменяемая операция может блокироваться на время ожидания примитива синхронизации, такого как 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.ManualResetEventSlim и System.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.Task и System.Linq.ParallelEnumerable — примеры классов, соответствующих этим рекомендациям. Дополнительные сведения см. в разделе "Отмена задачи" и "Практическое руководство. Отмена запроса PLINQ".

См. также