Это гибридный тип вещей? (Например, использует ли моя .NET-программа стек до тех пор, пока не выполнит асинхронный вызов, затем переключится на какую-то другую структуру, пока не будет завершена, после чего стек разматывается обратно в состояние, в котором он может быть уверен в следующих элементах и т. д.? )
В основном да.
Предположим, у нас есть
async void MyButton_OnClick() { await Foo(); Bar(); }
async Task Foo() { await Task.Delay(123); Blah(); }
Вот чрезвычайно упрощенное объяснение того, как продолжения продолжаются. Реальный код значительно сложнее, но это помогает понять идею.
Вы нажимаете кнопку. Сообщение ставится в очередь. Цикл обработки сообщений обрабатывает сообщение и вызывает обработчик щелчков, помещая адрес возврата очереди сообщений в стек. То есть, что происходит после того, как обработчик завершен, цикл сообщений должен продолжать работать. Так что продолжение обработчика - это цикл.
Обработчик щелчков вызывает Foo (), помещая адрес возврата в стек. То есть продолжение Foo - это остаток от обработчика щелчка.
Foo вызывает Task.Delay, помещая свой адрес возврата в стек.
Task.Delay выполняет любую магию, необходимую для немедленного возврата Задачи. Стек выпал, и мы снова в Foo.
Foo проверяет возвращенное задание, чтобы увидеть, выполнено ли оно. Не то. Продолжение в AWAIT является вызов Ла (), поэтому Foo создает делегат , который вызывает Л (), а также признаки того, что делегат в качестве продолжения выполнения этой задачи. (Я только что сделал небольшое неправильное заявление; вы его уловили? Если нет, мы расскажем об этом чуть позже.)
Затем Foo создает собственный объект Task, помечает его как незавершенный и возвращает его в стек обработчику щелчков.
Обработчик щелчков проверяет задачу Foo и обнаруживает, что она не завершена. Продолжением await в обработчике является вызов Bar (), поэтому обработчик щелчка создает делегат, который вызывает Bar (), и устанавливает его как продолжение задачи, возвращаемой Foo (). Затем он возвращает стек в цикл сообщений.
Цикл сообщений продолжает обрабатывать сообщения. В конечном итоге магия таймера, созданная задачей задержки, делает свое дело и отправляет в очередь сообщение о том, что теперь можно выполнить продолжение задачи задержки. Таким образом, цикл сообщений вызывает продолжение задачи, как обычно помещая себя в стек. Этот делегат зовет Бла (). Blah () делает то, что делает, и возвращает в стек.
Что теперь происходит? Вот хитрый момент. Продолжение задачи задержки не только вызывает Blah (). Он также должен вызвать вызов Bar () , но эта задача не знает о Bar!
Фактически Foo создал делегата, который (1) вызывает Blah (), а (2) вызывает продолжение задачи, которую Foo создал и передал обработчику событий. Вот как мы вызываем делегата, который вызывает Bar ().
И теперь мы сделали все, что нам нужно было сделать, в правильном порядке. Но мы никогда не прекращали обрабатывать сообщения в цикле сообщений очень долго, поэтому приложение оставалось отзывчивым.
То, что эти сценарии слишком сложны для стека, имеет смысл, но что заменит стек?
Граф объектов задач, содержащих ссылки друг на друга через классы замыкания делегатов. Эти классы замыкания являются конечными автоматами, которые отслеживают положение последнего выполненного ожидания и значения локальных объектов. Кроме того, в приведенном примере очередь действий в глобальном состоянии, реализованная операционной системой, и цикл сообщений, который выполняет эти действия.
Упражнение: как вы думаете, все это работает в мире без циклов сообщений? Например, консольные приложения. ожидание в консольном приложении совсем другое; Вы можете сделать вывод, как это работает из того, что вы знаете до сих пор?
Когда я узнал об этом несколько лет назад, стек был там, потому что он был молниеносным и легким, часть памяти, выделяемая приложению вне кучи, потому что она поддерживала высокоэффективное управление для выполняемой задачи (игра слов предназначена?). Что изменилось?
Стеки являются полезной структурой данных, когда время жизни активаций метода образует стек, но в моем примере активации обработчиков кликов, Foo, Bar и Blah, не образуют стек. И поэтому структура данных, которая представляет этот рабочий процесс, не может быть стеком; скорее это график распределенных кучи задач и делегатов, который представляет рабочий процесс. Ожидания - это точки в рабочем процессе, в которых невозможно продвинуться вперед в рабочем процессе, пока работа, начатая ранее, не будет завершена; пока мы ждем, мы можем выполнить другую работу, которая не зависит от тех конкретных запущенных задач, которые были выполнены.
Стек - это просто массив фреймов, где фреймы содержат (1) указатели на середину функций (где происходил вызов) и (2) значения локальных переменных и временных переменных. Продолжения задач - это одно и то же: делегат является указателем на функцию и имеет состояние, которое ссылается на конкретную точку в середине функции (где произошло ожидание), а замыкание имеет поля для каждой локальной или временной переменной , Кадры просто больше не образуют красивый аккуратный массив, но вся информация одинакова.