Snusmumriken
В чистых луях, как бы ты не изгалялся, ты не можешь вынести "ссылку на переменную из мёртвой корутины" куда либо ещё, если смог — эта ссылка не завязана на корутину, она как бы общая. S = {} function coroutined_stuff() local t = {blabla} S[1] = t end coroutine.wrap(coroutined_stuff)() Переменная t не относится к корутине, она создаётся в "куче" текущего стейса, и в корутине появляется просто ссылка на неё, когда корутина умирает — ничто не мешает ей остаться в S. Ну вот, ты ещё и используешь sol2, вот тут вот может быть что-то ужасное.
Александр
> Переменная t не относится к корутине, она создаётся в "куче", и в корутине просто ссылка на неё, когда корутина умирает — ничто не мешает ей остаться в S. Но каждая переменная относится к какому-то конкретному lua_State*, которых, как я выяснил, у меня много. Изначально я пытался запускать корутины на основном стейте без sol::thread, но больше одной одновременно работать не хотели ни в какую. У них был общий контекст/стек, поэтому при запуске второй корутины у меня просто глючила первая. Могу ошибаться, это наблюдения
Александр
Есть шанс что это тупое копирование стейта, с каким-то извращенским провешиванием ссылок.
lua_State *lua_newthread (lua_State *L); Creates a new thread, pushes it on the stack, and returns a pointer to a lua_State that represents this new thread. The new state returned by this function shares with the original state all global objects (such as tables), but has an independent execution stack.
Александр
> has an independent execution stack. что мне и требовалось для пула корутин
Snusmumriken
Хех, пришло время толкать лекцию на тему многопроцессорности.
Snusmumriken
Карочи, есть два основных типа "многопоточки": — Лёгкие треды (они же "зелёные"), это типа корутин. Хотя у разных ЯП бывают разные реализации, где-то кажется можно даже нарваться на "многопроцессорные" извращения, но не в луа. Их прекол в том, что это фактически другая организация кода. Как будто много-много наставленных goto (плюс свапы стеков и окружения). Их можно наплодить сто тыщ мильярдов, и они очень быстро свапаются, отлично заходит под асинхронный код, и его можно писать так как будто он нормальный и синхронный, правда если что-то заблокирует исполнение — оно будет заблокировано, и всё будет стоять: goto же, почти ничего больше. Луёвые корутины — именно такие. — Нормальные треды. Это когда язык требует у ОС, мол: "выдай мне тред, и загони в него вот это вот", например виртуальную машину скриптового языка вместе со скриптом. И операционная система уже им занимается: выделяет процессорное время на разных ядрах, помогает размечать память под каждый отдельный поток и так далее. Это полностью отдельный процесс, но если родительский грохнут, то детей через некоторое время вытрет ОС. Их не стоит плодить сто тыщ мильярдов, потому что свапы между потоками ОС проворачивает довольно медленно, плюс они начинают конкурировать за процессорное время и мешают друг другу, как правило, таких потоков (если они сильно нагружены) приложение создаёт не больше N - 1, где N — количество физических ядер цпу. Общение же между такими потоками обычно через пайпы ОС (пересылка строчек), сокеты (пересылка строчек) или шаред-мемори (общие участки памяти, как правило для хранения общих строчек, но иногда там хранят и структуры, вот извращенцы).
Александр
Я понимаю, что корутины не исполняются асинхронно. Мне это и не нужно. Мне просто нужно иметь много корутин одновременно, которые можно паузить и запускать без взаимного ущерба
Александр
И фактически у меня один поток (который системный, настоящий)
Snusmumriken
Так вот да, sol::thread видать создаёт отдельный ОС-поток, жди пока он не закончит работу полностью прежде чем что-то unref'ать.
Александр
Не создаёт он отдельный процесс, он вызывает https://pgl.yoyo.org/luai/i/lua_newthread
Snusmumriken
А как ты их одновременно запускаешь?
Александр
Под одновременно я подразумеваю, что есть пул и я в цикле последовательно их resume'лю. Завершившиеся удаляю, иногда добавляю новые
Александр
Одновременно существует больше одной запаузенной корутины
Snusmumriken
Хмм, тогда они исполняются не одновременно. Понял.
Александр
Ну да, извиняюсь, если запутал
Snusmumriken
Но тогда это ничем не отличается от local ROUTINES = {} function addRoutine(f, ...) ROUTINES[#ROUTINES + 1] = coroutine.create(f, ...) end function updateRoutines() for i = #ROUTINES, 1, -1 do local r = ROUTINES[i] if coroutine.status(r) == 'dead' then table.remove(ROUTINES, i) else coroutine.resume(r) end end end Я начинаю плохо понимать зачем нужны потоки и дочерние стейты.
Александр
Так это работать не должно
Александр
Я изначально ни о каких тредах и не задумывался. Но потом выяснилось, что каждой корутине нужен отдельный execution контекст. И я даже нашёл реализацию
Snusmumriken
Ой мама, а что мешает это вывести в луёвую часть?
Александр
А если вывести, то проблем не будет?
Snusmumriken
(нет)
Александр
Я уже и сам начал сильно сомневаться во всех своих убеждениях и знаниях
Snusmumriken
Самое простое — зашить луёвый скрипт строкой в плюсовый код, и исполнить его перед выдачей луа-стейта в работу через lua_call ))
Александр
Мне кажется логичным, что каждой корутине нужен отдельный execution контекст. Хранить стек, как минимум
Snusmumriken
Да, действительно, у каждой корутины есть свой стек и стейт областей видимости. Но помнится, дочерние луа-стейты не создаются.
Snusmumriken
А, пардон, я сел в лужу, корутины и есть дочерние стейты ))
Snusmumriken
Снус сломался, несите нового
Александр
Нет, лучше почините этого, иначе сломаюсь я
Александр
Ну тогда вопрос актуален - если у каждой переменной есть свой L, то есть и переменные с L от потока, где она была создана, то как происходит передача владения?
Snusmumriken
Вот щас смотрю.
Snusmumriken
Но в целом, корутины должны быть урезанным основным потоком. Создавать "глобальные" переменные в главном стейте, и только при лукапе смотреть на собственные локальные переменные. Я потому и подозревал что этого тут нема, потому что работаем с основным тредом практически постоянно.
Александр
https://sol2.readthedocs.io/en/latest/threading.html?highlight=main_object#working-with-multiple-lua-threads Во первой рамочке
Александр
Advanced used: Furthermore, for every single sol::reference derived type, there exists a version prefixed with the word main_, such as sol::main_table, sol::main_function, sol::main_object and similar. These classes, on construction, assignment and other operations, forcibly obtain the lua_State* associated with the main thread, if possible. Using these classes will allow your code to be immune when a wrapped coroutine or a lua thread is set to nil and then garbage-collected.
Александр
Из этого можно сделать вывод, что объект, который принадлежит стейту корутины, действительно может пережить корутину
Snusmumriken
Тады обnilяй всё содержимое корутины, а в конце вызываей collectgarbage().
Александр
Я уже обернул свою S в sol::main_table вместо sol::table, благодаря чему при присваивании S = some_coro_local_object владелец передаётся в основной стейт
Александр
collectgarbage() на каждый финал корутины - дороговато
Александр
Я уже сталкивался с проблемой отложенных деструкторов и сначала также её решил :D
Snusmumriken
Зато точно не переживёт : ) Пардон, сколько корутин ты плодишь/удаляешь по времени, что collectgarbage тебе дороговато?
Александр
Ну.. много. Сборка мусора занимает около 1мс по моим тестам. В критические моменты может быть до сотни корутин (некоторые без yield, поэтому создаются, выполняются и сразу умирают) в секунду. Сама корутина отрабатывает значительно быстрее, чем сборка мусора после неё (хотя я не замерял, но сделаю)
Snusmumriken
А можно это.. Как-то подольше? : )
Snusmumriken
Ну там, корутина принимает функцию-таску, исполняет, йелдит. Есть стабильный пул корутин.
Александр
Онлайн игра. Корутины - это события on_mob_death, on_mob_damage и прочие. Иногда on_damage - это просто total_damage = total_damage + damage, иногда сложнее (например, подождать секунду в yield'нутом состоянии и потом как-то отреагировать). Одного моба могут бить сразу сотня игроков по несколько ударов в секунду
Serezha
https://habr.com/ru/company/iponweb/blog/469049/
Snusmumriken
Онлайн игра. Корутины - это события on_mob_death, on_mob_damage и прочие. Иногда on_damage - это просто total_damage = total_damage + damage, иногда сложнее (например, подождать секунду в yield'нутом состоянии и потом как-то отреагировать). Одного моба могут бить сразу сотня игроков по несколько ударов в секунду
Кажется это точно не стоит запихивать в корутины. Я стараюсь даже таблицы не создавать на подобные штуки, потому что слишком много динамики, разметок и тут же удалений из кучи. Хм. Может я в чём-то ошибаюсь, но риальне чот много.
Александр
Можно конечно попробовать статичный пул, но нужно иметь возможность одновременной работы тысячи корутин, при этом средняя заполненность будет 1-3
Snusmumriken
Можно конечно попробовать статичный пул, но нужно иметь возможность одновременной работы тысячи корутин, при этом средняя заполненность будет 1-3
Просто саморасширяемый пул. Если тасок немного — по чуть-чуть уменьшаем, если буфер переполняется — досоздаём. Получается гибкая буферная фигня.
Александр
Хорошо, это как вариант решения проблемы. Но вопрос с владением объектов всё равно нужно как-то решить, чтобы я хотя бы понимал, что происходит.
Александр
Кстати таким решением я скорее всего воспользуюсь, спасибо. Скорость никогда не помешает
Александр
Например, что произойдёт, если в корутине будет создана переменная, помещена в глобальную таблицу, поток умрёт и будет сборка мусора? Всё равно ж ссылка невалидная
Snusmumriken
Должно быть ок, потому что она помещена в глобальную таблицу, куча типа общая.
Александр
Так у каждой переменной свой L!
Александр
Ну или может быть L: S L1: Q S[1] = Q; // владелец Q теперь L
Александр
Или как мне заставить все переменные аллоцироваться с владельцем L?
Александр
Можешь подкинуть кусок кода из ядра луа, где происходит аллокация объекта?
Snusmumriken
Странное дело, потому что тут я не в курсе. Если создаётся локальная переменная в корутине — я не знаю кому она принадлежит. Если она создаётся глобальной — она точно создаётся в мейн-стейте. Мне страшно это искать, потому что надо разбирать луёвый парсер.
Snusmumriken
Должно быть где-то тут )) https://www.lua.org/source/5.1/lcode.c.html
Александр
Да нужно только найти место аллокации объекта, чтобы понять, как он связывается со стейтом
Snusmumriken
Я дурак и смотрел в основном код стандартных библиотек, то есть "что дёргаем", а не "как именно дёргаем".
Александр
Так, начало положено
Александр
https://github.com/lua/lua/blob/72a094bda7d71050a91a88474d67d39aa2bc1c46/lapi.c#L511
Александр
Я пошёл от C-API
Александр
Далее, далее, далее
Александр
Финал
Александр
А вот и создание нового потока
Александр
Судя по всему, global_State у них у всех общий.
Anton
Привет всем. Допустим, существует некий глобальный стейт L. В нем есть какие переменные. Мы заспаунили тред, запустили в нем корутину, в ней создали какой-то объект a = { ... };. Правильно ли я понимаю, что "владелец" a - это не L, а некий дочерний стейт, стейт потока? Что произойдет с владельцем, если я запишу ссылку на a в глобальные переменные L, после чего поток и корутина умрут? Я получу мертвую ссылку и сегфолт в дальнейшем?
Вся аллокация и сборка мусора подвязаны на некий global_State, который и является экземпляром VM. Он не доступен пользовательскому коду, но создаётся в момент lua_open, который возвращает указатель на «главную корутину». Соответсвенно, каждая корутина обладает ссылкой на экземпляр VM, но с точки зрения GC это не очень важно, потому что «на самом деле» владеет управляемой памятью именно global_State. Впрочем, все lua_State используются сборщиком мусора для принятия решения о достижимости объекта. В таком дизайне нет, никакого висячего указателя не будет, пока GC может хоть как-то (через стеки корутин, через глобальную таблицу) достучаться до объекта.
Александр
Вся аллокация и сборка мусора подвязаны на некий global_State, который и является экземпляром VM. Он не доступен пользовательскому коду, но создаётся в момент lua_open, который возвращает указатель на «главную корутину». Соответсвенно, каждая корутина обладает ссылкой на экземпляр VM, но с точки зрения GC это не очень важно, потому что «на самом деле» владеет управляемой памятью именно global_State. Впрочем, все lua_State используются сборщиком мусора для принятия решения о достижимости объекта. В таком дизайне нет, никакого висячего указателя не будет, пока GC может хоть как-то (через стеки корутин, через глобальную таблицу) достучаться до объекта.
Отлично. Частично прояснилось, спасибо. Примерно к такому же выводу я пришёл в своих 3-4 последних сообщениях, где-таки добрался до global_State. Значит проблема или в моём коде, или в коде библиотечной абстракции. Но к каждой переменной всё равно привязан какой-то стейт, через который она обращается к глобальному стейту. Может ли протухнуть этот стейт?
Александр
Есть у нас глобальный стейт L, переменная a, созданная в корутине. a->L = L1 - дочерний стейт. G(a->L) == G(L). Допустим, мы копируем эту переменную в глобальный скоуп, в котором всё имеет L, но пришла переменная с L1, который может помереть в любой момент.
Snusmumriken
Типа всё что в корутинах отправляется в глобал-стейт, по хорошему.
Александр
Отправляется кем? Виртуальной машиной или разработчиком, который использует C-api?
Anton
Отлично. Частично прояснилось, спасибо. Примерно к такому же выводу я пришёл в своих 3-4 последних сообщениях, где-таки добрался до global_State. Значит проблема или в моём коде, или в коде библиотечной абстракции. Но к каждой переменной всё равно привязан какой-то стейт, через который она обращается к глобальному стейту. Может ли протухнуть этот стейт?
С точки зрения Lua VM никаких переменных нет, слоты на стеке. То есть утверждение не очень корректно. Это GC в VM ползает по стекам и решает, что протухло. Что может пойти не так? Например, у вас есть userdata, которая не заанкорена нигде в Lua-коде (и в registry ее нет). Вот тогда она будет сколлекчена и попытка обращения будет неудачна. Потому что за память userdata отвевает Lua VM, а за начинку – хост.
Александр
Не, юзердаты никакой нет. Есть sol::table table; в C++ (обёртка над табличкой), есть sol::table get_table() { return table; }, которая даёт ссылку в луа, где ей пользуются из корутин примерно так: get_table()[1] = "asd" Помимо этого есть ещё и void set_table(sol::table t) { table = t; }, который так же могут вызвать из луа как set_table({})
Александр
При unrefе этой самой table (когда все корутины точно мертвы, но возможно не собраны сборщиком) крашится внутри lua_rawgeti, так как index2addr возвращает NULL
Александр
Луа 5.3, если что
Max
Можно предположить, что код на плюсах был написан так, что был возможен следующий сценарий: 1) запомнили поинтер на таблицу 2) GC почистил таблицу внутри VM 3) в глобальную таблицу был помещён поинтер на очищенную таблицу
Александр
Там RAII везде и всюду, так на любой объект делается ref при создании/захвате и unref при выходе из скоупа.
Александр
Так что ситуация с забытой навешанной ссылкой крайне маловероятна
Александр
Первый Note отсюда явно предупреждает о том, что что-то может пойти не так с L.