Книга: Конкурентность в C#. Асинхронное, параллельное и многопоточное программирование. 2-е межд. изд.
Назад: Глава 9. Коллекции
Дальше: Глава 11. ООП, хорошо сочетающееся с функциональным программированием

Глава 10. Отмена

В фреймворке .NET 4.0 появилась обширная и хорошо спроектированная поддержка отмены. Эта поддержка является кооперативной; это означает, что отмена может запрашиваться, но не осуществляется принудительно в коде. Вследствие кооперативной природы отмены невозможно отменить код, если он написан без учета поддержки отмены. По этой причине рекомендую включать поддержку отмены как можно в большей части вашего кода.

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

Отмена рассматривается как специальная разновидность ошибки. По действующим правилам, отмененный код инициирует исключение типа OperationCanceledException (или производного типа — например, Task­Canceled­Exception). В этом случае вызывающий код знает, что отмена была замечена.

Чтобы сообщить вызывающему коду, что ваш метод поддерживает отмену, необходимо получить CancellationToken в параметре. Обычно такой параметр стоит на последнем месте, если только ваш метод не выдает уведомления о прогрессе (рецепт 2.3). Также можно рассмотреть возможность использования перегруженной версии или значения параметра по умолчанию для потребителей, не требующих отмены:

public void CancelableMethodWithOverload(CancellationToken

   cancellationToken)

{

  // Здесь размещается код.

}

 

public void CancelableMethodWithOverload()

{

  CancelableMethodWithOverload(CancellationToken.None);

}

 

public void CancelableMethodWithDefault(

    CancellationToken cancellationToken = default)

{

  // Здесь размещается код.

}

Значение CancellationToken.None представляет маркер отмены, который никогда не будет отменяться; это специальное значение, эквивалентное default(CancellationToken). Потребители передают это значение, если необходимость отмены операции не возникнет никогда.

У асинхронных потоков реализован похожий, но более сложный способ отмены. Отмена асинхронных потоков подробно рассматривается в рецепте 3.4.

10.1. Выдача запросов на отмену

Задача

Из вашего кода вызывается другой код, который может отменяться (для чего используется CancellationToken). Требуется отменить вызванный код.

Решение

Тип CancellationTokenSource является источником для CancellationToken. Он позволяет коду реагировать на запросы отмены; компоненты Cancel­lation­TokenSource позволяют коду выдать запрос на отмену.

Каждый объект CancellationTokenSource существует независимо от всех остальных (если только вы не свяжете их так, как сделано в рецепте 10.8). Свойство Token возвращает CancellationToken для этого источника, а метод Cancel выдает непосредственный запрос на отмену.

Следующий пример демонстрирует создание CancellationTokenSource, а также использование Token и Cancel. В коде используется async-метод, потому что его проще продемонстрировать в коротком примере; одна пара Token/Cancel используется для отмены всех видов кода:

void IssueCancelRequest()

{

  using var cts = new CancellationTokenSource();

  var task = CancelableMethodAsync(cts.Token);

 

  // В этой точке операция была запущена.

 

  // Выдать запрос на отмену.

  cts.Cancel();

}

В приведенном примере переменная task игнорируется после запуска; вероятно, в реальном приложении эта задача будет где-то сохранена и использована с await, чтобы пользователь знал о результате.

При отмене кода практически всегда возникает состояние гонки. Возможно, отменяемый код как раз собирался завершиться при выдаче запроса на отмену, и если перед завершением он не проверит свой маркер отмены, то код фактически завершится успешно. При отмене кода возможны три варианта: он может ответить на запрос на отмену (с выдачей исключения OperationCanceledException), он может завершиться успешно или же завершиться с ошибкой, не имеющей отношения к отмене (с выдачей другого исключения).

Следующий пример очень похож на предыдущий, не считая того, что он использует с задачей ключевое слово await, с демонстрацией всех трех возможных результатов:

async Task IssueCancelRequestAsync()

{

  using var cts = new CancellationTokenSource();

  var task = CancelableMethodAsync(cts.Token);

 

  // В этой точке операция выполняется.

 

  // Выдать запрос на отмену.

  cts.Cancel();

  // (Асинхронно) ожидать завершения операции.

  try

  {

    await task;

    // Если управление окажется в этой точке, значит, операция

    // была успешно завершена перед тем, как вступил в силу

    // запрос на отмену.

  }

  catch (OperationCanceledException)

  {

    // Если управление окажется в этой точке, значит, операция

    // была отменена до ее завершения.

  }

  catch (Exception)

  {

    // Если управление окажется в этой точке, значит, операция

    // завершилась с ошибкой перед тем как вступил в силу

    // запрос на отмену.

    throw;

  }

}

Обычно создание CancellationTokenSource и выдача запроса на отмену выполняются в разных методах. После того как вы отмените экземпляр CancellationTokenSource, он отменяется безвозвратно. Если понадобится другой источник, необходимо создать другой экземпляр. Ниже приведен более реалистичный пример приложения с графическим интерфейсом, в котором одна кнопка используется для запуска асинхронной операции, а другая кнопка — для ее отмены. Приложение также снимает и устанавливает блокировку кнопок StartButton и CancelButton, чтобы в любой момент времени выполнялась только одна операция:

private CancellationTokenSource _cts;

 

private async void StartButton_Click(object sender, RoutedEventArgs e)

{

  StartButton.IsEnabled = false;

  CancelButton.IsEnabled = true;

  try

  {

    _cts = new CancellationTokenSource();

    CancellationToken token = _cts.Token;

    await Task.Delay(TimeSpan.FromSeconds(5), token);

    MessageBox.Show("Delay completed successfully.");

  }

  catch (OperationCanceledException)

  {

    MessageBox.Show("Delay was canceled.");

  }

  catch (Exception)

  {

    MessageBox.Show("Delay completed with error.");

    throw;

  }

  finally

  {

    StartButton.IsEnabled = true;

    CancelButton.IsEnabled = false;

  }

}

 

private void CancelButton_Click(object sender, RoutedEventArgs e)

{

  _cts.Cancel();

  CancelButton.IsEnabled = false;

}

Пояснение

В самом реалистичном примере этого рецепта используется GUI-интерфейс, но не стоит думать, будто отмена предназначена исключительно для пользовательских интерфейсов. Отмена также находит применение на сервере; например, ASP.NET предоставляет маркер отмены, представляющий тайм-аут запроса или отсоединение клиента. Правда, источники маркера запроса на стороне сервера встречаются реже, однако ничего не мешает их использованию; они могут пригодиться, если вам понадобилось выполнить отмену по некоторой причине, на которую не распространяется отмена ASP.NET, — например, дополнительного тайм-аута для части обработки запроса.

Дополнительная информация

В рецепте 10.4 рассматривается передача маркеров async-коду.

В рецепте 10.5 рассматривается передача маркеров параллельному коду.

В рецепте 10.6 рассматривается использование маркеров в реактивном коде.

В рецепте 10.7 рассматривается передача маркеров сетям потоков данных.

10.2. Реагирование на запросы на отмену посредством периодического опроса

Задача

В коде имеется цикл, который должен поддерживать отмену.

Решение

Если в коде присутствует цикл обработки, то в нем нет низкоуровневых функций API, которым можно было передать CancellationToken. В этом случае необходимо периодически проверять, не был ли отменен маркер. Следующий пример периодически проверяет маркер в ходе выполнения цикла, создающего вычислительную нагрузку на процессор:

public int CancelableMethod(CancellationToken cancellationToken)

{

  for (int i = 0; i != 100; ++i)

  {

    Thread.Sleep(1000); // Некоторые вычисления.

    cancellationToken.ThrowIfCancellationRequested();

  }

  return 42;

}

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

public int CancelableMethod(CancellationToken cancellationToken)

{

  for (int i = 0; i != 100000; ++i)

  {

    Thread.Sleep(1); // Некоторые вычисления.

    if (i % 1000 == 0)

      cancellationToken.ThrowIfCancellationRequested();

  }

  return 42;

}

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

Пояснение

В большинстве случаев ваш код должен просто передать CancellationToken на следующий уровень. Примеры такого рода встречаются в рецептах 10.4–10.7. Метод периодического опроса (polling), использованный в этом рецепте, следует применять только в том случае, если у вас имеется вычислительный цикл, который должен поддерживать отмену.

У типа CancellationToken имеется другой метод IsCancellationRequested, который начинает возвращать true при отмене маркера. Некоторые разработчики используют его для реакции на отмену, обычно возвращая значение по умолчанию или null. Я не рекомендую использовать этот метод в большей части кода. В стандартном паттерне отмены выдается исключение OperationCanceledException, для чего вызывается метод ThrowIfCancellationRequested. Если код, находящийся выше в стеке, захочет перехватить исключение и действовать так, словно результат равен null, это нормально, но любой код, получающий CancellationToken, должен следовать стандартному паттерну отмены. Если вы решите не соблюдать паттерн отмены, по крайней мере четко документируйте свои намерения.

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

Дополнительная информация

В рецепте 10.4 рассматривается передача маркеров async-коду.

В рецепте 10.5 рассматривается передача маркеров параллельному коду.

В рецепте 10.6 рассматривается использование маркеров с реактивным кодом.

В рецепте 10.7 рассматривается передача маркеров сетям потоков данных.

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

В рецепте 10.1 рассматривается выдача запросов на отмену.

10.3. Отмена по тайм-ауту

Задача

Имеется код, который должен остановить выполнение после тайм-аута.

Решение

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

Также у источников маркеров отмены существуют вспомогательные методы, которые автоматически выдают запрос на отмену по таймеру. Тайм-аут, кроме того, можно передать конструктору:

async Task IssueTimeoutAsync()

{

  using var cts = new CancellationTokenSource

     (TimeSpan.FromSeconds(5));

  CancellationToken token = cts.Token;

  await Task.Delay(TimeSpan.FromSeconds(10), token);

}

Если у вас уже имеется экземпляр CancellationTokenSource, можно запустить тайм-аут для этого экземпляра:

async Task IssueTimeoutAsync()

{

  using var cts = new CancellationTokenSource();

  CancellationToken token = cts.Token;

  cts.CancelAfter(TimeSpan.FromSeconds(5));

  await Task.Delay(TimeSpan.FromSeconds(10), token);

}

Пояснение

Чтобы выполнить код с тайм-аутом, используйте CancellationTokenSource и CancelAfter (или конструктор). Той же цели можно добиться другими способами, но использование существующей системы отмены — самый простой и эффективный вариант.

Помните, что отменяемый код должен отслеживать состояние маркера отмены. Вам не удастся легко отменить код, для которого отмена не предусмотрена.

Дополнительная информация

В рецепте 10.4 рассматривается передача маркеров async-коду.

В рецепте 10.5 рассматривается передача маркеров параллельному коду.

В рецепте 10.6 рассматривается использование маркеров с реактивным кодом.

В рецепте 10.7 рассматривается передача маркеров сетям потоков данных.

10.4. Отмена async-кода

Задача

Вы используете async-код, для которого нужно обеспечить возможность отмены.

Решение

Простейший способ поддержки отмены в асинхронном коде — просто передать CancellationToken на следующий уровень. Следующий пример выполняет асинхронную задержку, после чего возвращает значение; для поддержки отмены маркер передается Task.Delay:

public async Task<int> CancelableMethodAsync(CancellationToken

   cancellationToken)

{

  await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);

  return 42;

}

Многие асинхронные API поддерживают CancellationToken, поэтому обес­печение отмены обычно сводится к простой передаче маркера. Как правило, если ваш метод вызывает функции API, получающие CancellationToken, то ваш метод также должен получать CancellationToken и передавать его всем функциям API, которые его поддерживают.

Пояснение

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

Отмена должна предоставляться как вариант там, где это возможно. Дело в том, что правильно реализованная отмена на высоком уровне зависит от правильно реализованной отмены на нижнем уровне. Таким образом, когда вы пишете собственные async-методы, постарайтесь как можно тщательнее обеспечить поддержку отмены. Никогда неизвестно заранее, какие высокоуровневые методы  будут вызывать ваш код, и им тоже может понадобиться отмена.

Дополнительная информация

В рецепте 10.1 рассматривается выдача запроса на отмену.

В рецепте 10.3 рассматривается использование отмены в качестве тайм-аута.

10.5. Отмена параллельного кода

Задача

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

Решение

Простейший способ поддержки отмены — передача CancellationToken параллельному коду. Параллельные методы поддерживают эту возможность посредством получения экземпляра ParallelOptions. Установка CancellationToken для экземпляра ParallelOptions выполняется так:

void RotateMatrices(IEnumerable<Matrix> matrices, float degrees,

    CancellationToken token)

{

  Parallel.ForEach(matrices,

      new ParallelOptions { CancellationToken = token },

      matrix => matrix.Rotate(degrees));

}

Также возможно отслеживать CancellationToken непосредственно в теле цикла:

void RotateMatrices2(IEnumerable<Matrix> matrices, float degrees,

    CancellationToken token)

{

  // Предупреждение: так поступать не рекомендуется; см. ниже.

  Parallel.ForEach(matrices, matrix =>

  {

    matrix.Rotate(degrees);

    token.ThrowIfCancellationRequested();

  });

}

Альтернативное решение требует большего объема работы и не так хорошо интегрируется, потому что параллельный цикл упаковывает OperationCanceledException в AggregateException. Кроме того, если Cancel­lation­Token передается в составе экземпляра ParallelOptions, класс Parallel сможет принять более разумные решения относительно частоты проверки маркера. По этим причинам маркер лучше передавать в параметре. В этом случае маркер также можно передать в тело цикла, но не следует только передавать маркер в тело цикла.

В Parallel LINQ (PLINQ) также предусмотрена встроенная поддержка отмены с оператором WithCancellation:

IEnumerable<int> MultiplyBy2(IEnumerable<int> values,

    CancellationToken cancellationToken)

{

  return values.AsParallel()

      .WithCancellation(cancellationToken)

      .Select(item => item * 2);

}

Пояснение

Поддержка отмены для параллельной работы — важный критерий хорошего пользовательского интерфейса. Если ваше приложение выполняет параллельную работу, оно создает серьезную нагрузку на процессор в течение хотя бы короткого времени. Высокий уровень использования процессора обычно заметен для пользователей, даже если не мешает работе других приложений на той же машине. Таким образом, я рекомендую поддерживать отмену при любых параллельных вычислениях (или любой другой работе, связанной с интенсивной нагрузкой на процессор), даже если общее время высокой нагрузки на процессор относительно невелико.

Дополнительная информация

В рецепте 10.1 рассматривается выдача запроса на отмену.

10.6. Отмена кода System.Reactive

Задача

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

Решение

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

private IDisposable _mouseMovesSubscription;

 

private void StartButton_Click(object sender, RoutedEventArgs e)

{

  IObservable<Point> mouseMoves = Observable

      .FromEventPattern<MouseEventHandler, MouseEventArgs>(

          handler => (s, a) => handler(s, a),

          handler => MouseMove += handler,

          handler => MouseMove -= handler)

      .Select(x => x.EventArgs.GetPosition(this));

  _mouseMovesSubscription = mouseMoves.Subscribe(value =>

  {

    MousePositionLabel.Content = "(" + value.X + ", " + value.Y + ")";

  });

}

 

private void CancelButton_Click(object sender, RoutedEventArgs e)

{

  if (_mouseMovesSubscription != null)

    _mouseMovesSubscription.Dispose();

}

System.Reactive довольно удобно использовать вместе с системой Cancel­lationTokenSource/CancellationToken, повсеместно применяемой для отмены. В оставшейся части этого рецепта рассматриваются возможности взаимодействия наблюдаемых объектов System.Reactive с CancellationToken.

Первый сценарий — наблюдаемый код, упакованный в асинхронный код. Базовое решение было рассмотрено в рецепте 8.5, и теперь вы хотите добавить поддержку CancellationToken. В общем случае проще всего выполнить все операции с использованием реактивных операторов, а затем вызвать ToTask для преобразования последнего полученного элемента в задачу, допускающую ожидание. Следующий пример показывает, как асинхронно получить последний элемент в последовательности:

CancellationToken cancellationToken = ...

IObservable<int> observable = ...

int lastElement = await

   observable.TakeLast(1).ToTask(cancellationToken);

// или: int lastElement = await observable.ToTask(cancellationToken);

Получение первого элемента выглядит очень похоже; просто измените наблюдаемый объект перед вызовом ToTask:

CancellationToken cancellationToken = ...

IObservable<int> observable = ...

int firstElement = await

   observable.Take(1).ToTask(cancellationToken);

Асинхронное преобразование всей наблюдаемой последовательности в задачу тоже происходит аналогично:

CancellationToken cancellationToken = ...

IObservable<int> observable = ...

IList<int> allElements = await

   observable.ToList().ToTask(cancellationToken);

Наконец, рассмотрим обратную ситуацию. Мы рассмотрели несколько решений для ситуаций, в которых код System.Reactive реагирует на CancellationToken, т.е. где запрос отмены CancellationTokenSource преобразуется в освобождение подписки. Также можно пойти в другом направлении: выдать запрос на отмену как реакцию на освобождение подписки.

Операторы FromAsync,  StartAsync и SelectMany поддерживают отмену, как показано в рецепте 8.6. Этих операторов достаточно для большинства практических ситуаций. Rx также предоставляет тип CancellationDisposable, который отменяет CancellationToken при освобождении. Вы можете использовать CancellationDisposable напрямую:

using (var cancellation = new CancellationDisposable())

{

  CancellationToken token = cancellation.Token;

  // Маркер передается методам, которые на него реагируют.

}

// В этой точке маркер отменяется.

Пояснение

В System.Reactive (Rx) есть собственная концепция отмены: освобождение подписок. В этом рецепте рассматриваются различные способы интегрировать Rx в универсальную структуру отмены, появившуюся в .NET 4.0. Пока вы находитесь в той части вашего кода, которая относится к миру Rx, используйте систему подписки/освобождения Rx; чтобы решение работало как следует, вводите поддержку CancellationToken только на границах.

Дополнительная информация

В рецепте 8.5 рассматриваются асинхронные обертки для кода Rx (без поддержки отмены).

В рецепте 8.6 рассматриваются обертки Rx для асинхронного кода (с поддержкой отмены).

В рецепте 10.1 рассматривается выдача запросов на отмену.

10.7. Отмена сетей потоков данных

Задача

Вы используете сети потоков данных. Требуется обеспечить поддержку отмены.

Решение

Лучшим способом поддержки отмены в вашем коде будет сквозная передача CancellationToken функциям API, поддерживающим отмену. У каждого блока в сети потока данных поддержка отмены является частью DataflowBlockOptions. Если вы захотите дополнить нестандартный блок данных поддержкой отмены, задайте свойство CancellationToken в параметрах блока:

IPropagatorBlock<int, int> CreateMyCustomBlock(

    CancellationToken cancellationToken)

{

  var blockOptions = new ExecutionDataflowBlockOptions

  {

    CancellationToken = cancellationToken

  };

  var multiplyBlock = new TransformBlock<int, int>(item => item * 2,

      blockOptions);

  var addBlock = new TransformBlock<int, int>(item => item + 2,

      blockOptions);

  var divideBlock = new TransformBlock<int, int>(item => item / 2,

      blockOptions);

 

  var flowCompletion = new DataflowLinkOptions

  {

    PropagateCompletion = true

  };

  multiplyBlock.LinkTo(addBlock, flowCompletion);

  addBlock.LinkTo(divideBlock, flowCompletion);

 

  return DataflowBlock.Encapsulate(multiplyBlock, divideBlock);

}

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

Пояснение

В сетях потоков данных отмена не является разновидностью сброса (flush). Когда блок отменяется, он теряет все свои входные данные и отказывается принимать новые элементы. Таким образом, если вы отменяете блок во время выполнения, это приведет к потере данных.

Дополнительная информация

В рецепте 10.1 рассматривается выдача запросов на отмену.

10.8. Внедрение запросов на отмену

Задача

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

Решение

В системе отмены .NET 4.0 предусмотрена встроенная поддержка этого сценария в виде связанных маркеров отмены. Источник маркера отмены может быть создан связанным с одним (или несколькими) существующими маркерами. Когда вы создаете источник связанного маркера отмены, полученный маркер будет отменяться при отмене любых из существующих маркеров или при явной отмене связанного источника.

Следующий пример выполняет асинхронный запрос HTTP. Маркер, переданный методу GetWithTimeoutAsync, представляет отмену, запрошенную конечным пользователем, а метод GetWithTimeoutAsync также применяет тайм-аут к запросу:

async Task<HttpResponseMessage> GetWithTimeoutAsync(HttpClient client,

    string url, CancellationToken cancellationToken)

{

  using CancellationTokenSource cts = CancellationTokenSource

      .CreateLinkedTokenSource(cancellationToken);

  cts.CancelAfter(TimeSpan.FromSeconds(2));

  CancellationToken combinedToken = cts.Token;

 

  return await client.GetAsync(url, combinedToken);

}

Полученный маркер combinedToken отменяется либо когда пользователь отменяет существующий маркер cancellationToken, либо при отмене связанного источника вызовом CancelAfter.

Пояснение

Хотя в предыдущем примере используется только один источник Cancel­lationToken, метод CreateLinkedTokenSource может получать любое количество маркеров отмены в своих параметрах. Это позволяет вам создать один объединенный маркер, на базе которого можно реализовать собственную логическую отмену. Например, ASP.NET предоставляет маркер отмены, представляющий отключение пользователя (HttpContext.RequestAborted); код обработчика может создать связанный маркер, который реагирует либо на отключение пользователя, либо на свои причины отмены (например, тайм-аут).

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

Дополнительная информация

В рецепте 10.1 рассматривается общий механизм выдачи запросов на отмену.

В рецепте 10.3 рассматривается использование отмены по тайм-ауту.

10.9. Взаимодействие с другими системами отмены

Задача

Имеется внешний или унаследованный код с собственными концепциями отмены. Требуется управлять им с использованием стандартного объекта CancellationToken.

Решение

У типа CancellationToken существует два основных способа реакции на запрос на отмену: периодический опрос (рассматривается в рецепте 10.2) и обратные вызовы (тема этого рецепта). Периодический опрос обычно используется для кода, интенсивно использующего процессор, — например, циклов обработки данных; обратные вызовы обычно используются во всех остальных ситуациях. Регистрация обратного вызова для маркера осуществляется методом CancellationToken.Register.

Допустим, вы пишете обертку для System.Net.NetworkInformation.Pingtype и хотите предусмотреть возможность отмены тестового опроса. Класс Ping уже имеет API на базе Task, но не поддерживает CancellationToken. Вместо этого тип Ping содержит собственный метод SendAsyncCancel, который может использоваться для отмены. Для этого зарегистрируйте обратный вызов, который активизирует этот метод:

async Task<PingReply> PingAsync(string hostNameOrAddress,

    CancellationToken cancellationToken)

{

  using var ping = new Ping();

  Task<PingReply> task = ping.SendPingAsync(hostNameOrAddress);

  using CancellationTokenRegistration _ = cancellationToken

      .Register(() => ping.SendAsyncCancel());

  return await task;

}

Теперь при запросе на отмену CancellationToken вызовет метод Send­Async­Cancel за вас, отменяя метод SendPingAsync.

Пояснение

Метод CancellationToken.Register может использоваться для взаимодействия с любой альтернативной системой отмены. Но следует помнить, что если метод получает CancellationToken, запрос отмены должен отменять только эту одну операцию. Некоторые альтернативные системы отмены реализуют отмену закрытием некоторого ресурса, что может привести к отмене нескольких операций; эта разновидность системы отмены плохо соответствует CancellationToken. Если вы решите инкапсулировать такую разновидность отмены в CancellationToken, следует документировать ее необычную семантику отмены.

Помните о сроке существования регистрации обратных вызовов. Метод Register возвращает отменяемый объект, который должен быть освобожден, когда обратный вызов перестанет быть нужным. Предыдущий пример использует команду using для выполнения завершающих действий при завершении асинхронной операции. Если в коде отсутствует команда using, то при каждом вызове кода с тем же (долгосрочным) маркером CancellationToken он будет добавлять новый обратный вызов (который, в свою очередь, будет поддерживать существование объекта Ping). Чтобы избежать утечки памяти и ресурсов, отмените регистрацию обратного вызова, когда он перестанет быть нужным.

Дополнительная информация

В рецепте 10.2 рассматривается реакция на маркер отмены посредством периодического опроса (вместо обратных вызовов).

В рецепте 10.1 рассматривается общий механизм выдачи запросов на отмену.

Назад: Глава 9. Коллекции
Дальше: Глава 11. ООП, хорошо сочетающееся с функциональным программированием