Правильный шаблон для удаления источника токена отмены - PullRequest
4 голосов
/ 22 апреля 2020

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

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

Поскольку источник токена отмены реализует IDisposable, мы должны вызвать его метод Dispose, поскольку мы закончили с ним. Смысл этого вопроса состоит в том, чтобы точно определить , когда вы завершили работу с данным источником токена отмены.

Предположим, что вы решили отменить текущую работу, вызвав метод Cancel для Источник токена отмены: необходимо ли ждать завершения текущей операции перед вызовом Dispose?

Другими словами, я должен сделать это так:

class Program 
{
  static void Main(string[] args) 
  {
    var cts = new CancellationTokenSource();
    var token = cts.Token;

    DoSomeAsyncWork(token); // starts the asynchronous work in a fire and forget manner

    // do some other stuff here 

    cts.Cancel();
    cts.Dispose(); // I call Dispose immediately after cancelling without waiting for the completion of ongoing work listening to the cancellation requests via the token

    // do some other stuff here not involving the cancellation token source because it's disposed
  }

  async static Task DoSomeAsyncWork(CancellationToken token) 
  {
     await Task.Delay(5000, token).ConfigureAwait(false);
  }
}

или так:

class Program 
{
  static async Task Main(string[] args) 
  {
    var cts = new CancellationTokenSource();
    var token = cts.Token;

    var task = DoSomeAsyncWork(token); // starts the asynchronous work in a fire and forget manner

    // do some other stuff here 

    cts.Cancel();

    try 
    {
      await task.ConfigureAwait(false);
    }
    catch(OperationCanceledException) 
    {
      // this exception is raised by design by the cancellation
    }
    catch (Exception) 
    {
      // an error has occurred in the asynchronous work before cancellation was requested
    }

    cts.Dispose(); // I call Dispose only when I'm sure that the ongoing work has completed

    // do some other stuff here not involving the cancellation token source because it's disposed
  }

  async static Task DoSomeAsyncWork(CancellationToken token) 
  {
     await Task.Delay(5000, token).ConfigureAwait(false);
  }
}

Дополнительные сведения: код, на который я ссылаюсь, написанный внутри веб-приложения ASP. NET core 2.2, здесь я использую сценарий консольного приложения просто для упрощения моего примера.

Я обнаружил аналогичные вопросы о стековом потоке, требующие избавления от объектов-источников отмены токенов. Некоторые ответы предполагают, что в некоторых случаях избавление от этого объекта на самом деле не требуется.

Мой подход ко всему предмету IDisposable заключается в том, что я всегда стремлюсь придерживаться открытого контракта класса, иными словами, если объект претендует на одноразовость, я предпочитаю всегда вызывать Dispose, когда я с этим покончено Мне не нравится идея угадывать, действительно ли требуется вызов dispose, в зависимости от деталей реализации класса, которые могут измениться в будущем выпуске без документов.

Ответы [ 3 ]

2 голосов
/ 22 апреля 2020

Чтобы гарантировать, что CTS (CancellationTokenSource), связанный с «забыть и забыть» Task, будет в конечном итоге ликвидирован, вы должны прикрепить продолжение к задаче и утилизировать CTS изнутри. продолжение. Это создает проблему, потому что другой поток может вызвать метод Cancel, когда объект находится в процессе его удаления, и согласно документации метод Dispose не является поточно-ориентированным:

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

Так что вызов Cancel и Dispose из двух разных потоков одновременно без синхронизации не вариант. Это оставляет только одну доступную опцию: добавить слой синхронизации вокруг всех publi c членов класса CTS. Однако это нежелательный вариант по нескольким причинам:

  1. Вы должны написать потокобезопасный класс-оболочку (код записи)
  2. Вы должны использовать его при каждом запуске отменяемого задание «забей и забудь» (напиши больше кода)
  3. понесет потерю производительности при синхронизации
  4. понесет потерю производительности присоединенных продолжений
  5. необходимость обслуживания системы, которая стал более сложным и более подверженным ошибкам
  6. Необходимость справиться с философским вопросом, почему класс не был спроектирован как ориентированный на многопоточность

Поэтому я рекомендую сделать альтернативу, которая заключается в простом отключении CTS, только в тех случаях, когда вы не можете дождаться завершения связанных с ним задач. Другими словами, если невозможно включить код, который использует CTS, в оператор using, просто позвольте сборщику мусора выполнить восстановление зарезервированных ресурсов. Это означает, что вам придется не повиноваться этой части документации:

Всегда вызывайте Dispose, прежде чем выпускать свою последнюю ссылку на CancellationTokenSource. В противном случае используемые им ресурсы не будут освобождены, пока сборщик мусора не вызовет метод Finalize объекта CancellationTokenSource.

... и this :

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

Если это заставляет вас чувствовать себя немного грязно, вы не одиноки , Вы можете почувствовать себя лучше, если считаете, что класс Task также реализует интерфейс IDisposable, но удаление экземпляров задачи не требуется .

2 голосов
/ 22 апреля 2020

Правильная практика - секунда - вы избавляетесь от CancellationTokenSource после того, как уверены, что задание отменено. CancellationToken полагается на информацию из CancellationTokenSource для правильной работы. Хотя текущая реализация CancellationToken написана таким образом, что она все равно будет работать, даже не вызывая исключений, если удаляется CTS, из которой она была создана, она может работать неправильно или всегда так, как ожидается.

1 голос
/ 22 апреля 2020

Как и любой IDisposable, вы утилизируете его, когда закончите с ресурсом. Это жесткое правило IDisposable, и я не сталкивался с ситуацией, когда это было не так, но я, конечно, открыт для обучения;).

В случае CancellationTokenSource это означает, что вы утилизируйте источник, когда сам объект и свойство Token больше не используются. (У меня только что был открыт источник для этого утверждения, но, увы, я отвлекся и каким-то образом потерял его)

Таким образом, вы решаете, когда задачи больше не используют CancellationToken. В вашем случае второй вариант, так как тогда вы уверены, что ни одна задача не использует токен.

edit; В дополнение к этому, рекомендуется также устанавливать нулевые свойства, которые реализуют одноразовые. в этом случае, поскольку у вас есть только локальные переменные, это не имеет значения, но когда у вас есть источник токена в виде поля или чего-то еще, убедитесь, что для поля установлено значение null, чтобы не было ссылок на источник токена.

...