Dmitry_Milk
> А действительно ли нужно несколько стеков? Ведь если внутри async-функции
> вызывается обычная функция - она обязана завершиться сразу же, нигде в этой
> иерархии вызовов уже не может возникнуть await
Зависит от языка, в некоторых async-функции ничем от обычных не отличаются.
Dmitry_Milk
> То есть, вроде бы достаточно присобаченнго к фьючеру состояния фрейма самой асинк-функции (и этот фрейм конечной величины, даже если включает в себя переменные всех областей видимости).
Точнее, идентификатор текущей точки ожидания + все видимые переменные. В случае рекурсивного вызова может быть неограниченного размера. Без рекурсии это конечный variant<Frame1, Frame2, ...> и если в языке есть продвинутое метапрограммирование, то можно автоматом сгенерировать функцию сериализации, пройдясь по всем членам.
}:+()___ [Smile]
> В случае рекурсивного вызова может быть неограниченного размера.
Если его сделать как Box<variant<...>>, то вроде все упрощается. Насчет эффективности - еще надо посмотреть на то, как часто надо мувить фьючер (и надо ли вообще). Бокс мувится элементарно, а с обычным значением - там еще и палки в колеса с невозможностью перемещения при наличии во фрейме ссылок на значения в том же фрейме (из-за чего, например, в том же расте пляски с Pin/Unpin).
Погуглил. Вроде в Rust и в Крестах используются беcстековые корутины. Думаю, в этом есть некий смысл, а значит, мне тоже имеет смысл делать их именно таковыми.
Нарыл в документации (нормальной, не для нубов) кое-что:
https://doc.rust-lang.org/reference/expressions/await-expr.html
Пока у меня есть следующее понимание: корутины, это на самом деле некие машины состояний, порождённые из линейной записи программы. Объект, порождённый "вызовом" async есть такая машина состояний, которая хранит в себе аргументы и локальные переменные а также текущую точку входа. Потом эту машину состояний (как некий функциональный объект) можно вызывать, при этом каждый вызов как-то изменяет её состояние. Вызывать её можно до тех пор, пока не будет достигнуто окончательное состояние, и при этом (тут я не до конца уверен) может получиться результат выполнения корутины.
Глядя на llvm-код примеров корутин из крестов я обнаружил следующее: там состояние корутины сохраняется в неком поле, при вызове корутины происходит переход к нужному месту в коде на основе значения состояния.
Генераторы работают вроде схожим образом. Там просто на каждом вызове возвращается некое значение.
Panzerschrek[CN]
> Генераторы работают вроде схожим образом
В питоне асинки эволюционировали из генераторов. Там до сих пор таском для асинк-бакэнда может быть как async-функция, так и генератор.
Panzerschrek[CN]
> Нарыл в документации (нормальной, не для нубов) кое-что
await в расте работает не с корутиной, а с более абстракной штукой - фьючером Future<T>, который и есть тот самый "Объект, порождённый "вызовом" async". То есть, корутину можно вызывать и без await (что можно сделать и из обычной функции), просто сразу же получив Future<T> вместо T. Правда потом с этим фьючером придется разбираться вручную (либо передать его выше)
А await просто сахар для "раздевания" Future<T> по его готовности.
Обнаружил такую не очень приятную вещь:
Все аргументы и локальные переменные корутины надо складывать в какую-то структуру, дабы они после вызова сохранялись. И если так прямо и делать, то всё будет даже нормально работать. Но есть один нюанс: будет некоторая переголова по размеру структуры. Ведь есть переменные с непересекающимся временем жизни, а ещё есть переменные, которые живут только от await до await и сохранять их вообще не надо.
В результате размер этой структуры может быть сильно больше размера стека фрейма для аналогичной простой функции. А ещё надо учесть, что при вызове корутины из корутины и все переменные последней тоже надо сохранять.
Panzerschrek[CN]
> Но есть один нюанс: будет некоторая переголова по размеру структуры. Ведь есть
> переменные с непересекающимся временем жизни
variant (а точнее аналог растовского enum), разбивая как по началам/концам областей видимости, так и по await-ам (когда разные варианты будут иметь одинаковую структуру). Тогда не придется отдельно диспетчить await-ы - нужный участок корутины автоматом будет диспетчиться при паттерн-матчинге этого варианта, а заодно - не придется думать о деструкторах.
Но вообще кажется, что лучше не идти по пути Раста и не реализовывать эту структуру как обычную strucr/enum языка, а считать ее специальной языковой конструкцией с особенными свойствами (скажем, ее нельзя мувить, если в корутине есть ссылки на переменные этой же корутины). Иначе как в Расте получится невозможным реализовывать async-бакэнды без unsafe (потому что ссылки в этой структуре приходится заменять указателями, которые потом надо разадресовывать).
Dmitry_Milk
> variant
Не подходит, ибо в различных состояний по крайней мере некоторые переменные будут присутствовать.
> не придется думать о деструкторах
Что значит не придётся? Очень даже придётся.
> не реализовывать эту структуру как обычную strucr/enum языка
Ну это чисто технический момент, как это по факту называть.
> скажем, ее нельзя мувить
Совсем забыл про это. Теперь понятно, зачем нужен Pin для вызова poll в Rust-овский корутинах.
И да, я язык с самого начала проектировал так, чтобы в нём неперемещаемых объектов не было, ибо неперемещаемость добавляет сложностей в дизайне языка. С корутинами придётся или таки вводить неперемещаемость, или делать вызов корутины unsafe, чтобы обязать программиста гарантировать отсутствие перемещения.
Panzerschrek[CN]
> чтобы в нём неперемещаемых объектов не было
А как же мютексы и атомики?
Их перемещение выглядит неправильно.
/A\
> А как же мютексы и атомики?
> Их перемещение выглядит неправильно.
А в чём их проблема? Что будет, если их переместить?
Справедливости ради, стоит заметить, что ни того ни другого (в привычном понимании) в Ü нету. Вместо atomic типов есть только atomic операции (чтение, модификация). Сместь мьютексов есть нечто вроде shared_ptr с rwlock внутри.
Panzerschrek[CN]
> С корутинами придётся или таки вводить неперемещаемость, или делать вызов корутины unsafe, чтобы обязать программиста гарантировать отсутствие перемещения.
Перемещаемость во многом эквивалентна сериализуемости, поэтому если осилишь сериализацию корутин, то автоматом получишь перемещаемость.
}:+()___ [Smile]
> то автоматом получишь перемещаемость
Суть перемещаемости - возможность тупо позвать memcpy. Свистопляски с исправлением ссылок после перемещения и прочие крестоизвращения не считаются.
Panzerschrek[CN]
> А в чём их проблема? Что будет, если их переместить?
Это не потокобезопасно. Эти объекты предполагают, что к ним обращаются из разных потоков, а при перемещении меняется их адрес, а значит нужен второй слой синхронизаций, который защищает сам объект от изменения адреса объекта синхронизации.
Перемещение атомика - копирование его значения, но сам атомик меняет свой адрес в памяти.
Перемещение мютекса еще сложнее, в винде это критическая секция размером в 32 байта что-ли, не уверен что ее можно копировать, и это точно плохо, когда мютекс заблокирован.
/A\
> а при перемещении меняется их адрес
А это уже детали реализации. Если нужен постоянный адрес, то можно объект в Box завернуть.
Крестоподход с неперемещаемым std::mutex - кривое говно, которое только вставляет палки в колёса.