У меня вопрос, почему [7] Task delay has been cancelled.
выполняется в том же потоке, где запрашивается аннулирование токена?
Это потому, что await
планирует продолжение своих задач с флагом ExecuteSynchronously
. Я также думаю, что это поведение удивительно, и первоначально сообщал об этом как об ошибке (закрыто как «по замыслу»).
Более конкретно, await
захватывает контекст, и , если этот контекст совместим с текущим контекстом, выполняющим задачу, то продолжение async
выполняется непосредственно в потоке, который завершает эту задачу. задача.
Чтобы пройти через это:
- Некоторые потоки пула потоков "7" запускаются
cancellationTokenSource.Cancel()
.
- Это заставляет
CancellationTokenSource
войти в отмененное состояние и запустить его обратные вызовы.
- Один из этих обратных вызовов является внутренним для
Task.Delay
. Этот обратный вызов не зависит от потока, поэтому он выполняется в потоке 7.
- Это приводит к отмене
Task
, возвращенного с Task.Delay
. await
запланировал свое продолжение из потока пула потоков, и все потоки пула считаются совместимыми друг с другом , поэтому продолжение async
выполняется непосредственно в потоке 7.
Напоминаем, что потоки пула потоков используются только при наличии кода для запуска. Когда вы отправляете асинхронный код, используя await
- Task.Run
, он может запустить первую часть (до await
) в одном потоке, а затем запустить другую часть (после await
) в другом потоке.
Таким образом, поскольку потоки пула потоков взаимозаменяемы, для потока 7 не является «неправильным» продолжение выполнения метода async
после await
; это только проблема, потому что теперь код после Cancel
заблокирован на этом async
продолжении.
Обратите внимание, что если я выполню cancellationTokenSource.Cancel () из основного потока, результат будет выглядеть как ожидалось
Это связано с тем, что контекст пользовательского интерфейса не считается совместимым с контекстом пула потоков. Поэтому, когда Task
, возвращаемый из Task.Delay
, отменяется, await
увидит, что он находится в контексте пользовательского интерфейса, а не в контексте пула потоков, поэтому он ставит свое продолжение в пул потоков вместо непосредственного его выполнения.
Интересно, что когда я заменил Task.Delay(TimeSpan.FromMinutes(1), cancellationTokenSource.Token)
на cancellationTokenSource.Token.ThrowIfCancellationRequested()
.NET, этот фоновый поток был занят, и вывод снова, как и ожидалось
Это не потому, что поток "занят". Это потому, что больше нет обратного вызова. Таким образом, метод наблюдения - опрос вместо того, чтобы уведомлять .
Этот код устанавливает таймер (через Task.Delay
), а затем возвращает поток в пул потоков. Когда таймер отключается, он захватывает поток из пула потоков и проверяет, отменен ли источник маркера отмены; если нет, он устанавливает другой таймер и снова возвращает поток в пул потоков. Смысл этого параграфа в том, что Task.Run
не представляет только «одну нить»; у него есть только поток во время выполнения кода (т.е. не в await
), и поток может измениться после любого await
.
<Ч />
Общая проблема await
с использованием ExecuteSynchronously
обычно не является проблемой, если вы не смешиваете блокирующий и асинхронный код. В этом случае лучшее решение - изменить код блокировки на асинхронный. Если вы не можете этого сделать, вам нужно быть осторожным, продолжая свои методы async
, которые блокируются после await
. Это в первую очередь проблема с TaskCompletionSource<T>
и CancellationTokenSource
. TaskCompletionSource<T>
имеет приятную опцию RunContinuationsAsynchronously
, которая переопределяет флаг ExecuteSynchronously
; к сожалению, CancellationTokenSource
нет; вам нужно будет поставить в очередь ваши Cancel
звонки в пул потоков, используя Task.Run
.
Бонус: викторина для ваших партнеров по команде .