Используется ли структура стека для асинхронных процессов?


10

На этот вопрос Эрик Липперт дает отличный ответ, описывая, для чего используется стек. В течение года я знал - вообще говоря - что такое стек и как он используется, но некоторые его ответы заставляют меня задуматься, не используется ли эта структура стека сегодня, где асинхронное программирование является нормой.

Из его ответа:

стек является частью овеществления продолжения в языке без сопрограмм.

В частности, без сопрограмм часть этого меня удивляет.

Он объясняет немного больше здесь:

Сопрограммы - это функции, которые могут запоминать, где они находились, некоторое время передавать управление другим сопрограммам, а затем возобновлять с того места, где они остановились позже, но не обязательно сразу после только что названного выхода сопрограмм. Подумайте о «yield return» или «await» в C #, который должен помнить, где они были, когда запрашивается следующий элемент или асинхронная операция завершается. Языки с сопрограммами или похожими языковыми функциями требуют более продвинутых структур данных, чем стек, для реализации продолжения.

Это отлично в отношении стека, но оставляет меня без ответа на вопрос о том, какая структура используется, когда стек слишком прост для обработки этих языковых функций, которые требуют более сложных структур данных?

Стек развивается по мере развития технологий? Что заменяет это? Это гибридный тип вещей? (Например, использует ли моя .NET-программа стек до тех пор, пока не выполнит асинхронный вызов, затем переключится на какую-то другую структуру, пока не будет завершена, после чего стек разматывается обратно в состояние, в котором он может быть уверен в следующих элементах и ​​т. д.? )

То, что эти сценарии слишком сложны для стека, имеет смысл, но что заменит стек? Когда я узнал об этом несколько лет назад, стек был там, потому что он был молниеносным и легким, часть памяти, выделяемая приложению вне кучи, потому что она поддерживала высокоэффективное управление для выполняемой задачи (игра слов предназначена?). Что изменилось?

Ответы:


14

Это гибридный тип вещей? (Например, использует ли моя .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) значения локальных переменных и временных переменных. Продолжения задач - это одно и то же: делегат является указателем на функцию и имеет состояние, которое ссылается на конкретную точку в середине функции (где произошло ожидание), а замыкание имеет поля для каждой локальной или временной переменной , Кадры просто больше не образуют красивый аккуратный массив, но вся информация одинакова.


1
Очень полезно, спасибо. Если бы я мог пометить оба ответа как принятый, я бы сделал это, но поскольку я не могу, я оставлю их пустыми (но не хотел, чтобы кто-то думал, что время для ответа не ценится)
jleach

3
@ jdl134679: Я бы посоветовал вам пометить что-то как ответ, если вы чувствуете, что на ваш вопрос ответили; это посылает сигнал, что люди должны прийти сюда, если они хотят прочитать хороший ответ, а не написать один. (Конечно, всегда рекомендуется писать хорошие ответы.) Мне все равно, кто получает галочку.
Эрик Липперт

8

Аппель написал, что старая сборка мусора на бумаге может быть быстрее, чем выделение стека . Читайте также его книгу « Сборка с продолжениями» и справочник по сбору мусора . Некоторые методы GC (неинтуитивно) очень эффективны. Прохождение стиль продолжения определяет каноническое целое программы преобразование (СУЗ преобразование ) , чтобы избавиться от стопок (концептуально заменяющих кадры вызовов с кучей выделяются затворами , другими слова «материализация» отдельные кадры вызова в виде отдельных «ценностей» или «объекты» ).

Но стек вызовов по-прежнему очень широко используется, и современные процессоры имеют выделенное оборудование (регистр стека, механизм кэширования и т. Д.), Выделенное для стеков вызовов (и это потому, что большинство языков программирования низкого уровня, особенно C, легче реализовать с помощью стека вызовов). Обратите также внимание , что стеки кэш дружественных (и это имеет значение много для производительности).

Практически, стеки вызовов все еще здесь. Но теперь у нас их много, и иногда стек вызовов разбивается на множество более мелких сегментов (например, по несколько страниц по 4 Кбайт каждый), которые иногда собирают мусор или выделяют кучу. Эти сегменты стека могут быть организованы в некоторый связанный список (или более сложную структуру данных, когда это необходимо). Так , например, GCC компиляторы имеют в -fsplit-stackвариант (особенно полезно для Go, и его «goroutines» и его «асинхронных процессов»). С разделенными стеками вы можете иметь много тысяч стеков (и совместные подпрограммы становятся проще в реализации), состоящих из миллионов небольших сегментов стека, и «раскручивание» стека может быть быстрее (или, по крайней мере, почти так же быстро, как с одним чанком) стек).

(другими словами, различие между стеком и кучей размыто, но может потребоваться преобразование всей программы или несовместимое изменение соглашения о вызовах и компилятора)

Смотрите также это и это, и многие статьи (например, это ), обсуждающие преобразование CPS. Читайте также о ASLR & call / cc . Читайте (& STFW) больше о продолжениях .

Реализации .CLR & .NET могут не иметь современного преобразования GC & CPS по многим прагматическим причинам. Это компромисс, связанный с целыми преобразованиями программы (и простотой использования низкоуровневых подпрограмм на Си и имеющих время выполнения, написанное на С или С ++).

Цыпленок Схема использует машинный (или C) стек нетрадиционным способом с преобразованием CPS: каждое выделение происходит в стеке, и когда оно становится слишком большим, происходит этап копирования и пересылки GC поколения, чтобы переместить последние выделенные значения стека (и, вероятно, текущее продолжение) до кучи, а затем стек резко уменьшается с большим setjmp.


Читайте также SICP , Прагматика языка программирования , Книга Дракона , Lisp In Small Pieces .


1
Очень полезно, спасибо. Если бы я мог пометить оба ответа как принятый, я бы сделал это, но поскольку я не могу, я оставлю их пустыми (но не хотел, чтобы кто-то думал, что время для ответа не ценится)
jleach
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.