Поработав некоторое время с асинхронным / ожидающим паттерном в C #, я внезапно осознал, что не знаю, как объяснить, что происходит в следующем коде:
async void MyThread()
{
while (!_quit)
{
await GetWorkAsync();
}
}
GetWorkAsync()
предполагается, что он возвращает ожидаемое, Task
что может вызвать или не вызвать переключение потока при выполнении продолжения.
Я не был бы смущен, если бы ожидание не было в петле. Естественно, я ожидаю, что остальная часть метода (то есть продолжение) будет потенциально выполняться в другом потоке, что нормально.
Однако внутри цикла концепция «остальной части метода» становится для меня немного туманной.
Что происходит с «остальной частью цикла», если поток включен в продолжение, если он не переключен? В каком потоке выполняется следующая итерация цикла?
Мои наблюдения показывают (не окончательно проверено), что каждая итерация начинается в том же потоке (исходном), а продолжение выполняется в другом. Это действительно может быть? Если да, то является ли это степенью неожиданного параллелизма, который необходимо учитывать для обеспечения безопасности потоков метода GetWorkAsync?
ОБНОВЛЕНИЕ: Мой вопрос не является дубликатом, как предлагают некоторые. while (!_quit) { ... }
Кодовый просто упрощение моего фактического кода. В действительности мой поток представляет собой долгоживущий цикл, который обрабатывает входную очередь рабочих элементов через регулярные промежутки времени (по умолчанию каждые 5 секунд). Фактическая проверка состояния выхода также является не простой проверкой поля, как предлагается в примере кода, а проверкой дескриптора события.
Ответы:
Вы можете проверить это в Try Roslyn . Ваш метод await переписывается в
void IAsyncStateMachine.MoveNext()
сгенерированный асинхронный класс.То, что вы увидите, выглядит примерно так:
По сути, не имеет значения, на какой нити вы находитесь; конечный автомат может возобновиться должным образом, заменив ваш цикл эквивалентной структурой if / goto.
При этом асинхронные методы не обязательно выполняются в другом потоке. Посмотрите объяснение Эрика Липперта «Это не волшебство», чтобы объяснить, как вы можете работать
async/await
только над одним потоком.источник
Во-первых, Servy написал некоторый код в ответе на аналогичный вопрос, на котором основан этот ответ:
/programming/22049339/how-to-create-a-cancellable-task-loop
Ответ Servy включает аналогичный
ContinueWith()
цикл с использованием конструкций TPL без явного использования ключевых словasync
andawait
; поэтому, чтобы ответить на ваш вопрос, подумайте, как может выглядеть ваш код, когда ваш цикл развернут, используяContinueWith()
Это займет некоторое время, чтобы обернуть голову, но в итоге:
continuation
представляет собой закрытие для «текущей итерации»previous
представляетTask
состояние, в котором находится «предыдущая итерация» (т.е. она знает, когда «итерация» закончена и используется для запуска следующей…)GetWorkAsync()
возвращает aTask
, это означает, чтоContinueWith(_ => GetWorkAsync())
будет возвращать,Task<Task>
следовательно, вызов, чтобыUnwrap()
получить «внутреннюю задачу» (то есть фактический результатGetWorkAsync()
).Так:
Task.FromResult(_quit)
- ее состояние начинается какTask.Completed == true
.continuation
запуск выполняется с использованиемprevious.ContinueWith(continuation)
continuation
обновления закрытия ,previous
чтобы отразить состояние завершения процесса_ => GetWorkAsync()
_ => GetWorkAsync()
завершено, оно «продолжает»_previous.ContinueWith(continuation)
- т.е.continuation
снова вызывает лямбдуprevious
было обновлено состояние,_ => GetWorkAsync()
поэтомуcontinuation
лямбда вызывается приGetWorkAsync()
возврате.continuation
Лямбда всегда проверяет состояние_quit
так, если_quit == false
то нет больше продолжений, иTaskCompletionSource
получает значение стоимости_quit
, и все завершается.Что касается вашего наблюдения относительно продолжения, выполняемого в другом потоке, это не то, что ключевое слово
async
/await
сделало бы для вас, согласно этому блогу «Задачи (все еще) не являются потоками, и асинхронность не параллельна» . - https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/Я бы предположил, что действительно стоит поближе взглянуть на ваш
GetWorkAsync()
метод в отношении потоков и безопасности потоков. Если ваша диагностика обнаруживает, что она выполнялась в другом потоке в результате вашего повторного асинхронного / ожидающего кода, то что-то в этом методе или связано с ним должно вызывать создание нового потока в другом месте. (Если это неожиданно, возможно,.ConfigureAwait
где-нибудь есть?)источник
SynchronizationContext
- это, безусловно, важно, так как.ContinueWith()
использует SynchronizationContext для отправки продолжения; это действительно объясняет поведение, которое вы видите, еслиawait
вызывается в потоке ThreadPool или потоке ASP.NET. В этих случаях продолжение, безусловно, может быть отправлено в другой поток. С другой стороны, вызоваawait
однопоточного контекста, такого как WPF Dispatcher или Winforms, должно быть достаточно, чтобы обеспечить продолжение в оригинале. нить