2021-10-12 04:11:51
Но, по правде говоря, прямо функции и классы в терминах конкретного языка виртуальная машина вам генерить не обязана, потому что они наружу не видны, но по смыслу там ровно это происходит. Но вот эта простая смена формы записи и есть та самая ментальная ступенька, о которой я говорил в начале, так как она сильнейшим образом развязывает руки и мозги геймплейному программисту в плане стейт-машин.
Но погодите. Генераторы это ещё не корутины. Код с yield-ами, что я показал выше, в такой форме использовать вообще-то не получится. Дело в том, что внутри указанных функций есть вложенные yield-ы. Поэтому в вызывающем коде придётся крутить циклы. Либо же возвращать вложенный генератор и написать обёртку, которая будет складировать генераторы в стек и разруливать их вызовы соответствующим образом. Это пишется очень легко и так сделаны, например, всем известные Unity-корутины. И вот это уже тянет на некое подобие нормальных асинхронных корутин.
«Асинхронных», потому что стек выстраивает отношения между генераторами по принципу «вызывающий-вызываемый». Но в принципе можно переключаться между генераторами не по логике стека, а как взбредёт в голову. И тогда такие корутины будут называться «синхронными». В том смысле, что отношения между ними симметричные, то есть все на равных правах. Так можно, например, завести параллельные функции, которые передают друг другу управление по принципу пинг-понга. Но это мы опять же уходим в сторону многопоточных историй, а в нашем спокойном и безопасном однопоточном месте такие выкрутасы не особо нужны. Вернёмся к нашим тотемным животным.
У нас всё ещё не полноценные корутины, а симулякр. Если мы захотим сделать yield где-то далеко на глубине стека вызовов в C#, нам хоть и не придётся теперь крутить цикл в каждом месте, но протаскивать yield всё же придётся через все уровни. Если бы корутины были реализованы на уровне языка, как, например, в Kotlin, то мы могли бы ещё немного улучшить жизнь нашим сурикатам, прокидывая yield с любой глубины. Это намного удобнее. Но не идеально, потому что в Kotlin все промежуточные функции придётся пометить как suspend. Тогда компилятор будет обращаться с этими функциями, как с генераторами (для их промежуточного состояния будет создаваться объект), но эти функции тогда будет нельзя вызывать из non-suspend функций. То есть помечая функцию как suspend, мы рискуем, что часть старого кода отвалится.
— А бывают ли вообще идеальные корутины? — спросите вы. — Чтобы вызываться без боли из любого места и не думать о том, какая там была функция.
Да, бывают. Мы наконец-то пришли к stack-full корутинам. Это когда вместо вот этой всей пурги про разрезание функций на много маленьких и создание структурок-состояний, мы просто берём и сохраняем целиком весь стек приложения. Сохраняем со всеми его обычными и волшебными функциями вообще без ограничений.
Я встречал stack-full корутины как минимум 2 раза. Этол boost-овые в С++. Но сами понимаете: С++ сам по себе такой, что «без боли» к нему слабоприменимое понятие. И, конечно же, в Lua. Это мои первые и самые любимые корутины, потому что корутины там, как говорится, first-class-citizen: они yield-ятся в любой момент и с любого уровня, не требуют менять существующий код и никак не мешают вызывать эти же функции вне корутин — одно удовольствие. Идеально для написания толп сурикат.
Но вы заметьте — и это главная мысль поста — куда мы пришли. Мы начали с разговора об однопоточных стейт-машинах, но закончили чем-то, ужасно напоминающим фиберы или горутины. И это то место, где разница между ними, а вместе с тем и понимание, стирается. В какой-то литературе между фиберами и корутинами проводят чёткую черту по наличию планировщика, в какой-то — это синонимы. Но так или иначе у очень многих в головах сидит ассоциация, что корутины это обязательно что-то про многопоточный код и состояние гонки.
Надеюсь, я развеял этот миф и эта статья открыла для кого-то корутины по-новому. Ну или по крайней мере стал сам лучше понимать эту область.
445 viewsedited 01:11