У меня возникла ситуация, когда звонок CancellationTokenSource.Cancel
не возвращается. Вместо этого, после вызова Cancel
(и до его возвращения) выполнение продолжается с кодом отмены кода, который отменяется. Если отмененный код не вызывает впоследствии какой-либо ожидаемый код, то вызывающий абонент, который изначально вызвал Cancel
, никогда не получает контроль. Это очень странно. Я бы ожидал, что Cancel
просто зарегистрирует запрос об аннулировании и сразу же сразу же будет независим от самой отмены. Тот факт, что поток, в котором вызывается Cancel
, заканчивает выполнение кода, который принадлежит к операции, которая отменяется, и делает это до возвращения к вызывающей стороне Cancel
, выглядит как ошибка в структуре.звонок в CancellationTokenSource.Cancel никогда не возвращается
Вот как это идет:
Существует кусок кода, давайте назовем это «рабочий код», который ожидает некоторую асинхронная коды. Для того, чтобы сделать простые вещи, скажем, этот код ожидает на Task.Delay:
try { await Task.Delay(5000, cancellationToken); // … } catch (OperationCanceledException) { // …. }
Как раз перед «рабочий код» вызывает Task.Delay
он выполняется на поток T1. Продолжение (то есть строка, следующая за «ожиданием» или блоком внутри catch) будет выполняться позже либо на T1, либо, возможно, на каком-то другом потоке, в зависимости от ряда факторов.
- Существует еще один фрагмент кода, назовем его «клиентским кодом», который решает отменить
Task.Delay
. Этот код вызываетcancellationToken.Cancel
. ВызовCancel
выполнен на резьбе T2.
Я бы ожидал, что нить T2 продолжит, возвращаясь к вызывающему абоненту Cancel
. Я также ожидаю, что содержимое catch (OperationCanceledException)
будет выполнено очень скоро в потоке T1 или в каком-то потоке, отличном от T2.
Что будет дальше, это удивительно. Я вижу, что на потоке T2, после вызова Cancel
, выполнение немедленно продолжается с блоком внутри catch (OperationCanceledException)
. И это происходит, когда Cancel
все еще находится в callstack. Это как если бы вызов Cancel
был захвачен кодом, который он отменяет. Вот скриншот Visual Studio показывает этот стек вызовов:
Более контекст
Вот еще несколько контекста о том, что делает фактический код: Там является «рабочим кодом», который аккумулирует Запросы. Запросы подаются некоторым «кодом клиента». Каждые несколько секунд «рабочий код» обрабатывает эти запросы. Запросы, которые обрабатываются, исключаются из очереди. Однако, «клиентский код» решает, что он достиг точки, когда он хочет, чтобы запросы обрабатывались немедленно. Чтобы сообщить об этом «рабочему коду», он вызывает метод Jolt
, который предоставляет «рабочий код». Метод Jolt
, который вызывается «кодом клиента», реализует эту функцию, отменяя Task.Delay
, который выполняется основным циклом кода рабочего. Код работника имеет Task.Delay
отменен и переходит к обработке запросов, которые уже были поставлены в очередь.
Фактический код был удален до его простейшей формы и код available on GitHub.
Environment
Проблема может быть воспроизведена в консольных приложений, фоновых агентов для универсального приложения для Windows, и фоновых агентов для универсального приложения для Windows Phone 8.1.
Проблема не может быть воспроизведена в универсальных приложениях для Windows, где код работает так, как я ожидал, и вызов Cancel
немедленно возвращается.
* Проблема не может быть воспроизведена в универсальных приложениях * - потому что в этом случае есть контекст синхронизации в потоке, где вы вызываете 'await Task.Delay (...)', поэтому продолжение, вызванное 'CancellationTokenSource.Cancel' асинхронно отправляется в этот контекст. Следовательно, нет тупика. – Noseratio