Поделиться через


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

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

Общий шаблон реализации модели совместной отмены:

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

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

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

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

Это важно

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

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

Источник токенов отмены и токены отмены

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

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

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

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

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

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

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

Типы отмены

Платформа отмены реализуется как набор связанных типов, которые перечислены в следующей таблице.

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

Модель отмены интегрирована в .NET в нескольких типах. Наиболее важными являются System.Threading.Tasks.Parallel, System.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...

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

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

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

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

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

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

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

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

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

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 в обратных вызовах. Если обратный вызов должен выполняться в определенном потоке, используйте System.Threading.CancellationTokenRegistration конструктор, позволяющий указать, что целевой syncContext является активным SynchronizationContext.Current. Ручное управление потоками в колбэке может привести к взаимоблокировке.

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

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

Если отменяемая операция может блокироваться, ожидая примитив синхронизации, такой как 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".

См. также