Вызов CancellationTokenSource.Cancel никогда не возвращается
у меня есть ситуация, когда вызов CancellationTokenSource.Cancel
никогда не возвращается. Вместо этого, после Cancel
вызывается (и до его возвращения) выполнение продолжается с кодом отмены кода, который отменяется. Если код, который отменен, впоследствии не вызывает ожидаемый код, то вызывающий, который первоначально вызвал Cancel
никогда не получает управление обратно. Это очень странно. Я бы ожидал Cancel
просто записать запрос отмены и возвратить немедленно независимый на сама отмена. Дело в том, что нить где Cancel
вызывается заканчивается выполнением кода, который принадлежит операции, которая отменяется, и он делает это перед возвращением вызывающему Cancel
похоже на ошибку в фреймворке.
вот как это происходит:
-
есть фрагмент кода, назовем его "рабочий код", который ждет некоторого асинхронного кода. Чтобы сделать вещи простыми, скажем, этот код ожидает на Задача.Задержка:
try { await Task.Delay(5000, cancellationToken); // … } catch (OperationCanceledException) { // …. }
непосредственно перед тем, как" рабочий код " вызывает Task.Delay
он выполняется в потоке T1.
Продолжение (то есть линия, следующая за "await" или блоком внутри catch), будет выполнено позже на T1 или, возможно, на каком-то другом потоке в зависимости от ряда факторов.
- есть еще один фрагмент кода, назовем его "клиентский код", который решает отменить
Task.Delay
. Этот код вызываетcancellationToken.Cancel
. ВызовCancel
сделано на потоке T2.
я ожидал бы, что поток T2 продолжится, вернувшись к вызывающемуCancel
. Я также ожидаю увидеть содержание catch (OperationCanceledException)
выполняется очень скоро в потоке T1 или в каком-либо потоке, отличном от T2.
что происходит дальше, удивляет. Я вижу это в потоке T2, после , выполнение немедленно продолжается с блоком внутри catch (OperationCanceledException)
. И это происходит в то время как Cancel
все еще находится в callstack. Это как если бы вызов Cancel
захвачен кодом, который он отменяется. Вот скриншот Visual Studio, показывающий этот стек вызовов:
больше контекста
вот еще один контекст о том, что делает фактический код:
Существует" рабочий код", который накапливает запросы. Запросы подаются неким "клиентским кодом". Каждые несколько секунд "рабочий код" обрабатывает эти запросы. Запросы обработки исключаются из очереди.
Однако время от времени" клиентский код " решает, что он достиг точки, где он хочет, чтобы запросы обрабатывались немедленно. Чтобы сообщить об этом" Рабочему коду", он вызывает метод Jolt
это" рабочий код " обеспечивает. Метод Jolt
который вызывается "клиентским кодом" реализует эту функцию, отменяя Task.Delay
это выполняется основным циклом кода рабочего. Код рабочего имеет свой Task.Delay
отменено и приступает к обработайте запросы, которые уже были поставлены в очередь.
фактический код был разделен до самой простой формы, и код доступно на GitHub.
окружающая среда
проблема может быть воспроизведена в консольных приложениях, фоновых агентах для универсальных приложений для Windows и фоновых агентах для универсальных приложений для Windows Phone 8.1.
проблема не может быть воспроизведена в универсальных приложениях для Windows, где код работает так, как я ожидал, и вызов Cancel
сразу возвращается.
2 ответов
CancellationTokenSource.Cancel
не просто установить IsCancellationRequested
флаг.
на CancallationToken
класс Register
метод, что позволяет регистрировать обратные вызовы,которые будут вызываться при отмене. И эти обратные вызовы вызываются CancellationTokenSource.Cancel
.
давайте посмотрим на исходный код:
public void Cancel()
{
Cancel(false);
}
public void Cancel(bool throwOnFirstException)
{
ThrowIfDisposed();
NotifyCancellation(throwOnFirstException);
}
здесь NotifyCancellation
способ:
private void NotifyCancellation(bool throwOnFirstException)
{
// fast-path test to check if Notify has been called previously
if (IsCancellationRequested)
return;
// If we're the first to signal cancellation, do the main extra work.
if (Interlocked.CompareExchange(ref m_state, NOTIFYING, NOT_CANCELED) == NOT_CANCELED)
{
// Dispose of the timer, if any
Timer timer = m_timer;
if(timer != null) timer.Dispose();
//record the threadID being used for running the callbacks.
ThreadIDExecutingCallbacks = Thread.CurrentThread.ManagedThreadId;
//If the kernel event is null at this point, it will be set during lazy construction.
if (m_kernelEvent != null)
m_kernelEvent.Set(); // update the MRE value.
// - late enlisters to the Canceled event will have their callbacks called immediately in the Register() methods.
// - Callbacks are not called inside a lock.
// - After transition, no more delegates will be added to the
// - list of handlers, and hence it can be consumed and cleared at leisure by ExecuteCallbackHandlers.
ExecuteCallbackHandlers(throwOnFirstException);
Contract.Assert(IsCancellationCompleted, "Expected cancellation to have finished");
}
}
хорошо, теперь подвох в том, что ExecuteCallbackHandlers
может выполнять обратные вызовы либо в целевом контексте, либо в текущем контексте. Я позволю вам взглянуть на ExecuteCallbackHandlers
метод исходный код как это немного слишком долго, чтобы включить здесь. Но самое интересное:
if (m_executingCallback.TargetSyncContext != null)
{
m_executingCallback.TargetSyncContext.Send(CancellationCallbackCoreWork_OnSyncContext, args);
// CancellationCallbackCoreWork_OnSyncContext may have altered ThreadIDExecutingCallbacks, so reset it.
ThreadIDExecutingCallbacks = Thread.CurrentThread.ManagedThreadId;
}
else
{
CancellationCallbackCoreWork(args);
}
Я думаю, теперь вы начинаете понимать, где я собираюсь искать дальше... Task.Delay
конечно. Давайте посмотрим на его исходный код:
// Register our cancellation token, if necessary.
if (cancellationToken.CanBeCanceled)
{
promise.Registration = cancellationToken.InternalRegisterWithoutEC(state => ((DelayPromise)state).Complete(), promise);
}
Хммм... что это?!--42-->InternalRegisterWithoutEC
метод?
internal CancellationTokenRegistration InternalRegisterWithoutEC(Action<object> callback, Object state)
{
return Register(
callback,
state,
false, // useSyncContext=false
false // useExecutionContext=false
);
}
Аргх. useSyncContext=false
- это объясняет поведение вы видите, как TargetSyncContext
свойство используется в ExecuteCallbackHandlers
будет false. Поскольку контекст синхронизации не используется, отмена выполняется на CancellationTokenSource.Cancel
контекст вызова.
это ожидаемое поведение CancellationToken
/Source
.
несколько похоже на how TaskCompletionSource
работает, CancellationToken
регистрация осуществляется синхронно с помощью вызывающего потока. Вы можете видеть это в CancellationTokenSource.ExecuteCallbackHandlers
это вызывается, когда вы отменяете.
гораздо эффективнее использовать тот же поток, чем планировать все эти продолжения на ThreadPool
. Обычно такое поведение не является проблемой, но это может быть, если вы называете CancellationTokenSource.Cancel
внутри замка как поток "захвачен", пока замок все еще взят. Вы можете решить такие проблемы с помощью Task.Run
. Вы даже можете сделать его методом расширения:
public static void CancelWithBackgroundContinuations(this CancellationTokenSource)
{
Task.Run(() => CancellationTokenSource.Cancel());
cancellationTokenSource.Token.WaitHandle.WaitOne(); // make sure to only continue when the cancellation completed (without waiting for all the callbacks)
}