Как выйти из корутины unity
Перейти к содержимому

Как выйти из корутины unity

  • автор:

unity coroutines or fibers?

Почитав http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4024.pdf вспомнились корутины в Unity и кажется они на самом деле fiber’ы, а не couroutine’ы. Например:

public class ExampleClass : MonoBehaviour < IEnumerator Coroutine1() < yield return new WaitForSeconds(5); // do something >IEnumerator Coroutine2() < yield return new WaitForSeconds(5); // do something >IEnumerator Start() < // 2 coroutines will be started in the next frame update // (in the same thread as each MonoBehavior.Update()) // and managed by scheduler (no need to manualy resume // a coroutine after it yields) StartCoroutine("Coroutine1"); StartCoroutine("Coroutine2"); >void Update() < // this will be called by the same thread // which calls Courotine1() or Coroutine2() // till each yield >> 

Для сравнения в Lua courutines, а не fibers:

 -- doesn't start anything co = coroutine.create(function () for i=1,10 do print("co", i) coroutine.yield() end end) -- runs coroutine in the same thread as the caller -- until first yield coroutine.resume(co) --> co 1 print(coroutine.status(co)) --> suspended coroutine.resume(co) --> co 2 coroutine.resume(co) --> co 3 -- more calls coroutine.resume(co) --> co 10 coroutine.resume(co) -- prints nothing 

dissident ★★
04.03.20 00:06:34 MSK

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

alysnix ★★★
( 04.03.20 02:11:49 MSK )
Ответ на: комментарий от alysnix 04.03.20 02:11:49 MSK

корутины не могут быть распаралелены?

abs ★★★
( 04.03.20 02:19:24 MSK )
Ответ на: комментарий от alysnix 04.03.20 02:11:49 MSK

Дело в том, что в Unity «не такие корутины». Т.е. yield есть, запускаются они в одном трэде, но контроль не возвращается пользователю. Если создать несколько корутин и в каждой сделать yield, то они просто будут между собой переключаться самостоятельно в одном из методов а-ля Update но не контролируемом пользователем.

Другими словами будет что-то вроде:

do each frame while not game over: yield from coroutine1 yeidl from coroutine2 . Update from 1 MonoBehavior Update from 2 MonoBehavior . 

(не уверен, что порядок именно такой, возможно Update’ы в начале, но работает примерно так).

Т.е. по-моему это все же fibers. Если же прочитать: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4024.pdf, то как раз под определение fiber попадает: нет мутексов, есть yield’ы, но и есть scheduler, нету preemtive multitasking, но есть cooperative multitasking, но контролировать пользователь его не может.

Хотелось уточнить, правильно ли я это понял.

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

Если верить pdf’у, то не совсем так:

  • трэды, они же нити — preemtive multitasking, контекст переключается шедулером, по таймеру или еще как-нибудь в любой точке кода (если не запрещено переключение тем или иным образом), и тут требуется полный набор объектов синхронизации
  • fibers, cooperative multitasking, в точках указанных пользователем (yield), но само переключение не контролируется пользователям.
  • coroutines, cooperative multitasking — тоже самое, но переключение между yield’ами производится пользователем

dissident ★★
( 04.03.20 02:19:27 MSK ) автор топика
Последнее исправление: dissident 04.03.20 02:24:41 MSK (всего исправлений: 1)

Ответ на: комментарий от alysnix 04.03.20 02:11:49 MSK

есть шедулер и есть весь набор обьектов синхронизации — это треды. в тредах yield тоже обычно есть, для принудительного переключения.

A single thread can support multiple fibers that are scheduled using a fiber level scheduler running within a single thread. (c) https://medium.com/software-design/boost-fiber-in-your-code-9dcdda70ca00

Т.е. и в fiber’ах и в корутинах нету preemtive multitasking, т.е. мутексы не нужны. И fiber’ы и корутины могут быть созданы в одном трэде, но переключение между ними может быть или scheduler’ом (fiber) или пользователем (coroutine).

dissident ★★
( 04.03.20 02:33:32 MSK ) автор топика
Ответ на: комментарий от abs 04.03.20 02:19:24 MSK

корутины не могут быть распаралелены?

корутины, в классическом понимании, это параллельность. там каждая корутина имеет свой стек. Только переключение контекста — то есть стекпоинтера, и программпоинтера, делается программистом ручками, вызовом yield. такая система максимально проста, там нет шедулера, мьютексов и всей тредовой кухни. Это удобно для небольших программ, но крайне неудобно для больших систем, еще и растущих. Тут уже надо пользовать нормальные треды.

alysnix ★★★
( 04.03.20 02:34:53 MSK )
Ответ на: комментарий от dissident 04.03.20 02:33:32 MSK

для нормальной человеческой жизни нужны полновесные треды, то есть preemptive multitasking — вытесняющая многозадачность. и соотвественно мьютексы, семафоры, крит секции(необязательно), условные переменные.

кооперативная многозадачность это корутины. это тоже паралелелизм, но этакий ручной.

одних корутин…ну или этих фиберов или как оно там… мало для правильного программирования.

alysnix ★★★
( 04.03.20 02:43:32 MSK )
Ответ на: комментарий от dissident 04.03.20 02:33:32 MSK

Т.е. и в fiber’ах и в корутинах нету preemtive multitasking, т.е. мутексы не нужны. И fiber’ы и корутины могут быть созданы в одном трэде, но переключение между ними может быть или scheduler’ом (fiber) или пользователем (coroutine).

  1. никакой параллелелизм невозможен без своего стека на каждую параллельную сущность. как бы она не называлась.
  2. теперь вопрос — кто переключает контексты, то есть дает возможность каждой сущности занять процессор и работать.

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

alysnix ★★★
( 04.03.20 03:01:36 MSK )
Ответ на: комментарий от alysnix 04.03.20 02:43:32 MSK

для нормальной человеческой жизни нужны полновесные треды, то есть preemptive multitasking — вытесняющая многозадачность. и соотвественно мьютексы, семафоры, крит секции(необязательно), условные переменные.

да-да, memory fences, cache locality, RCU, etc…

This is impossibly hard to get it right 

одних корутин…ну или этих фиберов или как оно там… мало для правильного программирования.

Ну вот в Unity оно таки работает. Например: есть match-3 игра, где есть разные supergem’ы вроде бомб, «уничтожателей горизонтальных линий», «уничтожателей вертикальных линий» и тому продобных. Они «распространяют свои эффекты» с разной скоростью. В зависимости от того, какой gem «ударен» каким эффектом (от какого типа supergem’а) начисляются разные очки.

При помощи корутин в Unity это очень просто делается, каждый эффект это корутина со своим yield WaitForSeconds(ThisGem.Speed). Каждый frame они либо делают что-то либо ждут. Бомба, например, может включить другой supergem с большей скоростью и в итоге еще третий gem будет первым ударен не первоначальным инициатором, а другим более быстрым supergem’ом. Последовательность корутин делает это автомагически (причем в single-threaded).

Мой вопрос касался того, что по-моему корутины в Unity неправильно названы и это все таки fibers (в отличие от yield в Python/Lua/etc).

dissident ★★
( 04.03.20 03:04:27 MSK ) автор топика
Ответ на: комментарий от alysnix 04.03.20 03:01:36 MSK

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

Шедулер для данного трэда. Т.е. просто цикл:

for each coroutine do it until yield or dead go to next coroutine 

Либо другой тип шедулера.

Это и будут fiber. А корутины если самостоятельно нужно запускать resume после каждого yield.

если шедулер по таймеру — это вытесняющая многозадачность. если переключение по явному(или неявному) вызову yield из самого кода — это кооперативная. как можно неявно вызвать yield?

Не «неявно вызвать yield», а «явно вызвать resume» (см. пример Lua выше) или «неявно вызвать resume» (см пример Unity выше).

dissident ★★
( 04.03.20 03:08:15 MSK ) автор топика
Последнее исправление: dissident 04.03.20 03:21:20 MSK (всего исправлений: 1)

Ответ на: комментарий от dissident 04.03.20 03:04:27 MSK

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

alysnix ★★★
( 04.03.20 03:10:53 MSK )
Ответ на: комментарий от alysnix 04.03.20 03:10:53 MSK

ну… не все же программируют бомбы в юнити.:) разумная жизнь есть и за ее пределами.

Ну можно вообще все написать вот так:

Output MyFunction(Input input) const

И все будет immutable. И никакие трэды не нужны. И мутексы и прочее. Как я понял fibers/coroutine позволяют писать таки human-readable async код, а не Haskell, но при этом избежав мутексов, но при этом без блокировок.

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

Ну в PDF из ссылок выше хорошо описано.

Fiber: Один трэд создает их сколько угодно. В них есть yield. Мутексы и прочее не нужны. Каждый yield передает управление выбранному шедулеру для данного трэда (например просто циклу, который вызываеть следущий fiber до его yield и так для каждого fiber, пока они все не издохнут). Тип шедулера можно выбрать (просто цикл, хитроумный цикл и т.д.)

Coroutine: Один трэд создает их сколько угодно. В них есть yield. Мутексы и прочее не нужны. Каждый yield передает управление трэду, который их создал. Их нужно руками резюмить. Сколько и какие решает писатель кода этого самого трэда.

Coroutine можно превратить в fiber написав scheduler.

Я просто хотел понять:

  • Прав ли я, что корутины в Unity — это fibers?
  • Правильно ли я понял эти определения и примеры вообще?

dissident ★★
( 04.03.20 03:17:39 MSK ) автор топика
Последнее исправление: dissident 04.03.20 03:22:17 MSK (всего исправлений: 5)

Ответ на: комментарий от dissident 04.03.20 03:17:39 MSK

короче так. корутины в юнити, это сильно похоже на «функции с несколькими точками входа», они же бесстековые корутины. у них возможен yield только на верхнем уровне, то есть только в теле корутины, но не в внутри функции вызванной из тела. таким штукам стек не нужен, в силу ограничения, что выход(как бы переключение) только на верхнем уровне. это далеко от обычной(стековой) корутины и фибера. что там в луа — не смотрел

alysnix ★★★
( 04.03.20 04:24:37 MSK )
Ответ на: комментарий от dissident 04.03.20 03:17:39 MSK

ну а в луа(если посмотреть по диагонали), обычные стековые корутины. там все ручками переключается, в любом месте вроде.

alysnix ★★★
( 04.03.20 04:31:55 MSK )
Ответ на: комментарий от alysnix 04.03.20 04:24:37 MSK

Я не уверен, что они бесстековые (см. ниже примеры с int steps и CustomYieldInstruction).

Еще я хотел обратить внимание, что у них шедулер есть. Ну тот же, что каждый frame запускает update для MonoBehavior навешанных на GameObject.

Т.е. например в игре есть DoomGuy (GameObject), у него есть Shotgun (MonoBehavior) и Legs (MonoBehavior), Legs запускают две корутины WalkLeftLeg(), WalkRightLeg(), тогда каждый фрейм будет вызываться:

Shotgun.Update(); Legs.Update(); WalkLeftLeg().ResumeTillYield(); // psedocode WalkRightLeg().ResumeTillYield(); // psedocode 

«без всякого усилия само по себе».

Поэтому IMHO это fiber’ы. Пусть и бесстековые. Хотя почему бесстековые. Я же в начале корутины могу объявить int steps и оно будет в стэке корутины. И если я ему буду делать steps++ как-нибудь так:

class Legs: public MonoBehavior < IEnumerator WalkLeftLeg() < int steps = 0; while (true) < yield return null; steps++; >> IEnumerator WalkRightLeg() < int steps = 0; while (true) < yield return null; steps++; >> IEnumerator Start() < StartCoroutine("WalkLeftLeg"); StartCoroutine("WalkRightLeg"); >void Update() < // do something >> 

steps будет инкрементироваться после каждого вызова Resume() для каждой «корутины ноги».

Или я неправильно понимаю понятие «бесстековая корутина»?

Что касается yield() на верхнем уровне — это не до конца так. Можно создать свою yield instruction, которую yield’ить из своей корутины: https://docs.unity3d.com/ScriptReference/CustomYieldInstruction.html. Так например реализованы всякие WaitForSeconds(int). В своей корутине можно делать yield new WaitForSeconds(5) и это как бы и будет корутина, которая вызывает другу корутину и yield’ит ее результат. Если под стэком имелось ввиду это, то он есть.

В общем IMHO — это fiber’ы. Пусть и трэд один (тот, что их дергает каждый фрейм).

dissident ★★
( 04.03.20 05:08:53 MSK ) автор топика
Последнее исправление: dissident 04.03.20 05:14:02 MSK (всего исправлений: 3)

Ответ на: комментарий от alysnix 04.03.20 04:31:55 MSK

ну а в луа(если посмотреть по диагонали), обычные стековые корутины. там все ручками переключается, в любом месте вроде.

Да, в Lua нужно ручками дергать co.resume(). Но ничего не мешает написать шедулер (правда трэд только один в Lua, так что придется делать а-ля Unity, этакий event queue, скрипт будет просто вертеться в while (true) и делать resume() корутинам одну за одной, кроме того можно добавить список корутин и тогда корутины могут в своем теле добавлять в queue новые корутины). Этаки https://gameprogrammingpatterns.com/event-queue.html. MultiTasking without threads!

Кстати папоминает PeekMessage из WinAPI. Так что не только это можно использовать для match-3 game.

dissident ★★
( 04.03.20 05:19:45 MSK ) автор топика
Ответ на: комментарий от dissident 04.03.20 05:19:45 MSK

Не очень только понятно, чем это всё отличается в концепции от event loop?

menangen ★★★★★
( 04.03.20 06:55:03 MSK )
Ответ на: комментарий от menangen 04.03.20 06:55:03 MSK

Не очень только понятно, чем это всё отличается в концепции от event loop?

Я понимаю это так. что Event Loop будет или однотрэдовая вроде (допустим DispatchMessage* внутри содержит switch (msg.type) и что-то делает):

while (true) < msg = GetMessage(); // это делает работу в том же трэде, что и loop // и может добавить новые Message в queue DispatchMessage(msg); >

Или async много/дву/пулл-трэдовая (с callback’ами) вроде:

while (true) < msg, calback = GetMessageWithCallback(); // это делает работу в отдельном трэде или там // в одном из трэдов из трэд пула и может добавить новые // Message в queue DispatchMessageAsyncInNewThread(msg, callback); >

Если мы добавим fibers, то получиться однотрэдовый loop с yield’ами, что позволит переключать stack между конкретными DispatchMessage, что-нибудь вроде:

while (true) < msg = GetMessage(); // это делает работу и может добавить новые Message // но обслуживание конкретных msg в switch (msg.type) // иногда делает yield, позволяя внешнему циклу идти // дальше и делать другую работу в том же трэде, что // и loop, но со своим stack'ом DispatchMessageFiber(msg); yield(); // сюда еще можно всобачить >void DispatchMessageFiber(msg) < switch (msg.type) < case SOME_TYPE: DoSomeType(); break; case SOME_OTHER_TYPE: DoSomeOtherType(); break; . >> void DoSomeType() < while (some_condition) < DoSomeTypeALittleBit(); yield(); >> // same for DoSomeOtherType() 

Как-то так. Если я правильно понимаю.

PS Выше pseudocode.

PPS Я посмотрел «видосик» про boost::fibers: https://www.youtube.com/watch?v=e-NUmyBou8Q, но IMHO из-за того, что везде такие абстракции как boost::fibers::async труднее понять, что происходит, чем если бы был просто yield.

PPPS А да, весь смысл в том, что однотрэдовая loop однотрэдовая, многотредовая делает много context switch, а cooperative multitasking позволяет их не делать. Т.е. even loop был просто как пример, что и там можно корутины/файберы впихнуть.

dissident ★★
( 04.03.20 07:50:27 MSK ) автор топика
Последнее исправление: dissident 04.03.20 07:58:55 MSK (всего исправлений: 3)

Ответ на: комментарий от dissident 04.03.20 05:08:53 MSK

безстековая корутина — это stackless coroutine, можно погуглить. безстековая — это значит что у нее нет независимого стека, как у нормальной реально паралельной сущности — треда, фибера, или стековой корутины. Когда я в начале говорил о корутинах, я даже не считал эти «безстековыми корутины», корутинами.

alysnix ★★★
( 04.03.20 12:22:57 MSK )
Ответ на: комментарий от dissident 04.03.20 07:50:27 MSK

безстековая работает примерно так — это просто функция которой передается ее внтуренний контекст в качестве скрытого параметра, с которым она вышла из себя ранее. а оператор выхода yield у нее работает так — он берет текущий контекст — то есть состояние переменных и адрес команды куда надо будет войти и возвращает его, при это она сама завершается, и стек чистый. при повторном вызове это корутины, по контексту состояние переменных восстанавливается и просто осуществляется «longjump» на требуемое место в коде,и она как бы продолжается.

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

alysnix ★★★
( 04.03.20 12:36:56 MSK )
Ответ на: комментарий от alysnix 04.03.20 02:34:53 MSK

Это удобно для небольших программ, но крайне неудобно для больших систем, еще и растущих. Тут уже надо пользовать нормальные треды.

Это не так. От размера системы корутины не зависят, тем более корутины можно использовать в тредах. Корутины скорее про неблокируемый контекст. Например, чтение из сокета лучше ложится в модель корутин, чем треды, т.к. в линуксе треды фактически полноценный процесс.

xpahos ★★★★★
( 04.03.20 16:46:17 MSK )
Ответ на: комментарий от xpahos 04.03.20 16:46:17 MSK

сначала хорошо бы определиться что такое корутина, поскольку часто этим светлым именем называют все подряд.

вот например «безстековая корутина» — это оксюморон. это скорее маркетинговый ход, чем реальная корутина.

при вводе-выводе блокировка будет всегда(в общем случае), и кто-то должен встать в ожидание. потому что «блокировка» это и есть постановка в ожидание некоего события текущего треда или корутины(если она умеет вставать на обьект ожидания). полинг в этом случае не рассматриваем, поскольку это неверная реализация реакции на асинхронное событие.

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

alysnix ★★★
( 04.03.20 20:56:58 MSK )
Ответ на: комментарий от alysnix 04.03.20 20:56:58 MSK

Ты код хоть раз писал? Корутина без полинга работать не может. Все блокировки выводятся в отдельный тред и оттуда полятся. Если корутина может заблокироваться на сисколе, то это уже файбер, тем более гугловый, т.к. только в гугле есть провязка с ядром.

xpahos ★★★★★
( 04.03.20 22:43:05 MSK )
Ответ на: комментарий от alysnix 04.03.20 20:56:58 MSK

По-моему у тебя немного каша в голове (как немного и у меня, для чего я этот топик и создал — чтобы прояснить вопрос):

Корутины — это просто способ писать однотрэдовый код, в котором как раз и будет что-то вроде полинга. Точно так же его можно написать используя тот сам полинг. yield — это просто конструкция, позволяющая написать этот код более читаемо, в одном месте:

void Coroutine() < while (!Poll()) < yield(); >DoSmth(); > 

Конструкции вроде await, позволяют написать вот это, превращая блокирующую функцию в неблокирующую:

void NonBlockingFunctionUsingCoroutineRead()

Здесь вместо полинга есть отдельный трэд, но тот трэд который его вызывает, вместо callback’ов (как в каком-нибудь javascript):

do_smth((function() < do_smth_else(function() < do_yet_another_thing(); >); >); do()

Вместо этой каши используя корутины можно написать:

Корутины можно использовать и без какого-то трэда «снизу», т.е. не только оборачивая блокирующую операцию read() (правда в этом случае она сама должна быть корутиной а не просто функцией), в этом смысле они похожи на обычную функцию, с той разницей, что передают управление вверх. Стэковые они при этом или безстековые — деталь реализации (определаяющая лишь то, можно ли из одной корутины вызвать другую):

void Coroutine1() < while (true) < DoSmth1(); yield(); >> void Coroutine2() < while (true) < DoSmth2(); yield(); >> Coroutine1 cor1; Coroutine2 cor2; while (true)

Это равнозначно такому коду:

while (true)

Если корутины будут стэковые, то из Coroutine1 можно вызвать Coroutine2 и например если есть две корутины, которые обе ждут окончания третьей, то если третья закончится, то и первая и вторая закончатся.

Превратить псевдокод выше в волокна (fibers):

void Coroutine1() < // same as above >void Coroutine2() < // same as above >SetSomeKindOfManagerWithPrioritiesForExampleAndRun()

Т.е. и корутины и волокна — это просто возможность написать однотрэдовый код, в котором передается управление между корутинами/волокнами в точках, заданных пользователем (yield, await и т.п.). Стэковые они при этом или нет — вопрос второй. Я так понимаю, что особого смысла в безстэковых файберах нету, так как это будут просто безстэковые корутины, к котором прилеплен цикл (простой или хитроумный, например с приоритетами, с возможностью выбора типа цикла), который легко написать и руками. Я не вижу проблемы, в том, чтобы написать стэковые корутины, гда одна может вызвать другую, но какой в этом смысл? Вероятно с безстэковыми корутинами и хитроумным шедулером легко создать deadlock (даже если трэд один). Поправьте меня кто-нибудь plz — если это не так?

То, что в boost::fibers использовано похожее API, что и для трэдов (boost::fibers::async например) — просто уровень абстракции, внутри все равно использующий yield.

Ни мутексы, ни тому подобные вещи ни в корутинах, ни в файберах не нужны, так как трэд один (ну или несколько трэдов, в каждом из которых какое-то количество корутин/файберов).

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

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

Корутину можно использовать как для того, чтобы ждать блокирующую операцию, так и просто для выполнения разных кусков кода так, чтобы они были в одном трэде, но при этом были написаны «в одном месте». Добавление шедулера и стэковости позволяет использовать корутины практически как трэды (потому в boost::fibers и такое же API как в C++ трэдах).

Корутины можно превратить в файберы добавив шедулер и (вероятно) стэк. Файберы в корутины можно превратить не используя шедулер а переключая их вручную.

Переключение стэка так или иначе будет делаться при помощи запоминания переменных и т.п., разница в том, будет оно рекурсивным или нет. IMHO самое важное здесь то, что в этом случае:

void Function1() < await Coroutine(); >void Function2() < await Coroutine(); >void Coroutine()

И Function1 и Function2 дождались Coroutine(). Без стэка этого осуществить IMHO нельзя. Поправьте меня пожалуйста кто-нибудь если я не прав?

dissident ★★
( 05.03.20 17:13:04 MSK ) автор топика
Последнее исправление: dissident 05.03.20 17:23:05 MSK (всего исправлений: 1)

Ответ на: комментарий от xpahos 04.03.20 22:43:05 MSK

Ты код хоть раз писал? Корутина без полинга работать не может.

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

 -- doesn't start anything co = coroutine.create(function () for i=1,10 do print("co", i) coroutine.yield() end end) -- runs coroutine in the same thread as the caller until first yield coroutine.resume(co) --> co 1 coroutine.resume(co) --> co 2 

Это выглядит как просто конструкция языка, с тем что alysnix описывал тут unity coroutines or fibers? (комментарий), т.е. просто конструкция, которая сохраняет значения переменных, адрес возврата и т.п. и возвращает управление не в return, а yield.

Шедулер же должен «опрашивать» корутины, чтобы знать, какие закончены, какие нет. Это имелось ввиду?

dissident ★★
( 05.03.20 18:41:47 MSK ) автор топика
Ответ на: комментарий от dissident 05.03.20 17:13:04 MSK

По-моему у тебя немного каша в голове (как немного и у меня, для чего я этот топик и создал — чтобы прояснить вопрос):

Корутины — это просто способ писать однотрэдовый код, в котором как раз и будет что-то вроде полинга. Точно так же его можно написать используя тот сам полинг. yield — это просто конструкция, позволяющая написать этот код более читаемо, в одном месте:

а давай для ликвидации каши в твоей голове, сначала вики почитаем:

Сопрограмма (англ. coroutine) — программный модуль, особым образом организованный для обеспечения взаимодействия с другими модулями по принципу кооперативной многозадачности: модуль приостанавливается в определённой точке, сохраняя полное состояние (включая стек вызовов и счётчик команд), и передаёт управление другому, тот, в свою очередь, выполняет задачу и передаёт управление обратно, сохраняя свои стек и счётчик. Наряду с фиберами (англ. fiber), сопрограммы являются средством обеспечения «легковесной» программной многопоточности в том смысле, что могут быть реализованы без использования механизмов переключений контекста операционной системы.

ты видишь тут — про сохранение стека? так вот. у каждой уважащей себе корутины свой свой ОТДЕЛЬНЫЙ стек(случай безстековой корутины не рассматриваем — это оксюморон),и в этом смысле она подобна треду или фиберу. и никакая это не «однотредовая» программа с поллингом и тепе. ты блуждаешь в трех соснах потому, что всякие юнити и луа тебя запутали своми терминологиями. в юнити ВООБЩЕ не корутины. смотри корутины в вики. вот я кусок текста привел тебе.

alysnix ★★★
( 05.03.20 19:05:01 MSK )
Ответ на: комментарий от dissident 05.03.20 18:41:47 MSK

Под полингом я имел в виду не полинг состояния корутин, а полинг состояния абстракций с которыми они работают.

xpahos ★★★★★
( 05.03.20 19:33:43 MSK )
Ответ на: комментарий от xpahos 05.03.20 19:33:43 MSK

Под полингом я имел в виду не полинг состояния корутин, а полинг состояния абстракций с которыми они работают.

А если они вовсе не работают ни с какими абстракциями как пример Lua выше а просто делают что-то -> отдают управление по yield -> делают что-то -> отдают управление по yield…

Где в этом примере полинг?

dissident ★★
( 05.03.20 20:57:42 MSK ) автор топика
Ответ на: комментарий от alysnix 05.03.20 19:05:01 MSK

Ну во-первых вики — не достоверный источник.

Во-вторых стэк так или иначе сохранять надо, имея возможность вызывать из корутины другую или нет, даже в твоем описании безстэковых корутин ты говоришь о сохранении значений переменных — разве это не стэк? Другое дело — рекурсивный он или нет.

В-третьих, в юнити, кстати, как я уже упоминал, одна корутина может вызвать другую (см CustomYieldInstruction).

В-четвертых, и если в юнити не корутины, то в Lua тогда что? Я не вижу разниц между примерами Unity/Lua в самом первом своем сообщении/вопросе кроме того, что в Lua корутины нужно руками запускать, а в Unity их запускает шедулер (тот самый run loop, который так же запускает Update() каждый фрейм), почему я и спросил fiber’ы они или нет. Но сейчас подумав немного прихожу к выводу, что таки нет, так как шедулер выбирать нельзя, да и не до конца это шедулер, а main loop.

В-пятых, «дяденька я не сварщик, а только каску нашел» (с) анекдот. Я сам хочу разобраться. В том, что ты говорил есть как минимум несколько вещей, которые не соответствуют тому, что я читал/смотрел. Например, что для fiber’ов (lightweight threads) нужны мутексы и прочие способы синхронизации, тогда как они не нужны.

В-шестых, я сам не до конца понимаю, что делает корутину волокном (fiber) кроме шедулера. Явно возможность делать так, что если две корутины обе ждут третью — то все работает как-то с этим связано. А это в свою очередь явно связано с рекурсивностью стэка. Или под стэком ты имеешь ввиду только стэк вызова функций, а не переменные и их состояние? Ну так это с точки зрения ассемблера не правильно, в стэке лежат как переменные так и адреса возврата из функций и их параметры. Так или иначе я хочу разобраться, а не флеймить. Я прочел/посмотрел вот это:

  • http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4024.pdf
  • https://medium.com/software-design/boost-fiber-in-your-code-9dcdda70ca00
  • https://medium.com/software-development-2/coroutines-and-fibers-why-and-when-5798f08464fd
  • https://youtu.be/e-NUmyBou8Q
  • https://youtu.be/YYtzQ355_Co
  • https://www.lua.org/pil/9.1.html

И так до конца не понимаю разницу между реализацией корутин vs волокон. Конкретно, не понимаю, что нужно для того, чтобы разруливать ситуацию где от одной корутины зависят множество других и появляется граф — что необходимо, чтобы это работало с любым типом шедулера (обычный цикл, шедулер с приоритетом, шедулер, который «join»ит родительские корутины, когда закончены вызванные ними, шедулер, который «join»ит все корутины только когда ни одной не осталось и т.д.) и не было deadlock’ов.

А ты мне вики в нос тычешь.

Может вот это что-то прояснит:

(не прочел пока). Но это про корутины в C++, а они там убогие (раньше вообще назывались resumable functions и были реализованы первыми Microsoft’ом). Т.е. с boost::fibers это мало связано.

dissident ★★
( 05.03.20 21:13:28 MSK ) автор топика
Последнее исправление: dissident 05.03.20 21:16:03 MSK (всего исправлений: 2)

Ответ на: комментарий от dissident 05.03.20 21:13:28 MSK

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

alysnix ★★★
( 05.03.20 22:22:48 MSK )
Ответ на: комментарий от alysnix 05.03.20 22:22:48 MSK

Вот IMHO тут надо не по диагонали. И уж точно не вики. У нас, кстати, в институте препод был, который говорил, что если ему кто-нибудь википедию процитирует в работе или ответе, то сразу 2 поставит.

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

Это не отвечает на вопрос, что именно необходимо, чтобы превратить безстэковую корутину в волокно (fiber), кроме шедулера и возможности его выбрать, что можно легко дописать руками (псевдо-c++ и с захардкоженным «шедулером»):

queue> queue; void coroutine(int id, int count, function next = function()) < while (count--) < cout if (next) < queue.push(next); >> // named after order of start of execution auto cor1 = bind(coroutine, 1, 42); auto cor3 = bind(coroutine, 3, 43); // these two add another coroutine (cor3) in it's body (next) auto cor2 = bind(coroutine, 2, 44, cor3); auto cor4 = bind(coroutine, 4, 45, cor3); queue.push(cor1); queue.push(cor2); // simple "scheduler" for current thread, just a loop // (should be in a separate thread, but for simplicity, // for the sake of this simple example, it is in the // same thread as the main thread and, before starting // it, some coroutines are already added (cor1 and cor2) // to make it all go round) while (!finished) // assume some other thread sets this flag < while (queue.empty() && !finished) < co_yield(); >if (!finished) < auto cor = queue.front(); queue.pop(); cor(); >> 

IMHO этого недостаточно для полноценных волокон. Особенно если добавить какие-нибудь co_await (чтобы cor2 и cor4 зависели от выполнения cor3 при помощи co_await, а не просто закидывали их в queue).

dissident ★★
( 05.03.20 23:44:47 MSK ) автор топика
Последнее исправление: dissident 06.03.20 00:13:53 MSK (всего исправлений: 8)

Ответ на: комментарий от dissident 05.03.20 23:44:47 MSK

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

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

то, что в юнити — это не корутины вообще. такую дурь я б даже реализовывать не стал(и меня бы не попросили). пафосное название «безстековые корутины», это такой же пафос как — «безалкогольное пиво».

если тебе надо узнать как делать треды и корутины на голом железе, я могу вкраце пояснить. если не надо, то непонятно о чем мы спорим. то, что ты пишешь тут как самостройные корутины — это вовсе не они. верней, какой-то частный случай, явно неудобный. и вообще надо сначала определиться — зачем они нужны вообще, и что с их помощью ты хочешь решить. хочешь написать свой кернел — возьми какой-нить evaluation board и напиши его. будет еще один самостройный кернел.

alysnix ★★★
( 06.03.20 01:37:40 MSK )
Ответ на: комментарий от alysnix 06.03.20 01:37:40 MSK

Ну и толку от этого сообщения? Одни пальцы веером, а информации никакой. 🙁

dissident ★★
( 06.03.20 02:01:54 MSK ) автор топика
Ответ на: комментарий от dissident 05.03.20 23:44:47 MSK

минимальный нормальный интерфейс для класса корутина на с++ таков: дисклеймер — как код вставить не знаю пока. вставил по простому

void run()=0; //тело корутины void start(); //запуск корутины //два способа переключения static void yield(); //переключение на следующую корутину в списке активных корутин static void yield(Coroutine *to); переключение на именно указанную 

после чего ты наследуешь свой классы от корутины и пишешь им свой run. потом говоришь им co->start(), и внутри кода, в произвольном месте, переключаешься или одним способом или другим. так выглядят корутины в самом общем случае, и именно такие они полезны более всего. поскольку код можно писать внутри как хочешь, не забывать только переключаться.

alysnix ★★★
( 06.03.20 02:05:11 MSK )
Ответ на: комментарий от alysnix 06.03.20 02:05:11 MSK

и еще. посольку нормальные корутины должны вставать в ожидание, им таки нужен один обьект под названием ну допустим Signal.

class Signal < bool wait(time_t timeout); //корутина ждет сигнала с таймаутом void raise(); //просигналить - те, кто ждут будут активированы >

после чего например прием события с клавиатуры работает просто (это я описываю для некоего самостройного кернела) — при нажатии клавиши возникает аппартное прерывание и контроллер прерываний вызывает обработчик по данному вектору. обработчик кладет в переменную KeyCode код клавиши и делает raise() обьекту класса Signal, на который встала корутина(функция сигнала — wait), что читает код из переменной KeyCode и чета там по ним делает. сделав что надо, она опять встает в wait на этот сигнал. вот и все. уже можно програмировать асинхронный ввод-вывод и всякие не особо тривиальные штучки. но для полной радости нужна вытесняющая многозадачность и полновесные треды с мьютексами и условными переменными.

alysnix ★★★
( 06.03.20 02:29:00 MSK )
Ответ на: комментарий от alysnix 06.03.20 02:05:11 MSK

дисклеймер — как код вставить не знаю пока. вставил по простому

Когда пишешь сообщение внизу есть ссылка Markdown, открой ее, там будет инфа по разметке (в частности по вставлению кода):

после чего ты наследуешь свой классы от корутины и пишешь им свой run. потом говоришь им co->start(), и внутри кода, в произвольном месте, переключаешься или одним способом или другим. так выглядят корутины в самом общем случае, и именно такие они полезны более всего. поскольку код можно писать внутри как хочешь, не забывать только переключаться.

А как из них сделать fiber?

dissident ★★
( 06.03.20 05:05:19 MSK ) автор топика
Ответ на: комментарий от alysnix 06.03.20 02:29:00 MSK

И как ты будешь yield/raise имплементировать (тот самый stack), если ты говоришь, что запоминания адресов переменных недостаточно (так как это будут безстэковые корутины)? swapcontext? Не получится ли это так же тяжеловесно как трэды?

EDIT: А нет, вроде можно обойтись goto:

  • https://stackoverflow.com/questions/24202890/switch-based-coroutines
  • https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html
  • https://www.boost.org/doc/libs/1_51_0/doc/html/boost_asio/example/http/server4/coroutine.hpp

dissident ★★
( 06.03.20 05:13:17 MSK ) автор топика
Последнее исправление: dissident 06.03.20 05:20:03 MSK (всего исправлений: 2)

Ответ на: комментарий от dissident 06.03.20 05:13:17 MSK

Ладно, с этим вроде понятно: добавить стэк и шедулер. Непонятно, как его реализовать.

EDIT: А нет, вроде можно обойтись goto:

Не понимаю я этих реализаций с goto. 🙁 Из того, что описано здесь: https://blog.panicsoftware.com/coroutines-introduction/ следует, что без swapcontext stackful coroutines не сделаешь.

dissident ★★
( 06.03.20 08:33:04 MSK ) автор топика
Ответ на: комментарий от dissident 06.03.20 08:33:04 MSK

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

стек захватывается в функции корутины — start,и после его приготовления(там надо кое что поместить на вершину стека),и она запускается на этом стеке.

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

alysnix ★★★
( 06.03.20 12:40:16 MSK )
Ответ на: комментарий от dissident 05.03.20 20:57:42 MSK

Очевидно, что в такой ситуации корутины используют какую-то lock free структуру для обработки cpu bound задач. В такой ситуации не работал с корутинами, не знаю есть ли в этом случае профит.

xpahos ★★★★★
( 06.03.20 20:54:25 MSK )
Ответ на: комментарий от alysnix 06.03.20 12:40:16 MSK

Поправь меня, пожалуйста, если я что-то неверно понял.

У fiber’ов может быть шедулер (как и у безстэковых корутин, твоего «оксюморона», но это IMHO не имеет смысла, см. ниже), например, https://www.boost.org/doc/libs/1_65_1/libs/fiber/doc/html/fiber/scheduling.html. Имеется ввиду не то, что шедулер определяет, когда прервать fiber, что было бы вытесняющей многозадачностью (это определяет сам fiber, в точках вызова yield, что является кооперативной многозадачностью), а в том, что fiber’ы не нужно вручную «возобновлять» при помощи resume — их «возобновляет» шедулер (в случае если он есть и используется).

При этом fiber’ы могут быть симметричные и несимметричные. В случае несимметричных fiber’ов yield возвращает управление вызвавшему fiber трэду, а в случае симметричных они переключаются между друг другом.

Но как в случае симметричных, так и даже несимметричных fiber’ов можно реализовать шедулер. Например в случае несимметричных корутин (тех, которые возвращают управление вызвавшему трэду) шедулер будет «resume»ить fiber’ы согласно какому-нибудь алгоритму в этом самом трэде, который можно будет выбрать, например, в простейшем случае это будет просто цикл:

queue fibers; while (!finished) < while (fibers.empty() && !fibers) < co_yield(); >if (!finished) < fiber_type f = fibers.front(); fibers.pop(); f.resume(); >> 

Или это будет, например, цикл с приоритетами:

struct fiber_type < int prio; . void operator()() < . >>; struct greater_fib < bool operator()(const fiber_type& a, const fiber_type& b) const< return a.prio >b.prio; > >; vector fibers; fibers.push_back(. ); fibers.push_back(. ); . // sort by priorities make_heap(fibers.begin(), fibers.end()); while (!finished) < while (fibers.empty() && !fibers) < co_yield(); >// sort by priorities to keep fibers sorted in case any of the executed fiber // adds something to the heap (not effective, but this is just an example) make_heap(fibers.begin(), fibers.end()); if (!finished) < fiber_type f = fibers.front(); fibers.pop(); f.resume(); >> 

В случае безстэковых корутин компилятор при создании каждой корутины генерирует и создает при помощи new структуру (где хранятся, например, значения переменных в данный момент) для корутины (в принципе, не обязательно компилятор эту структуру генерирует, безстэковые корутины могут быть реализованы в качестве библиотеки, правда в этом случае необходимо будет вручную задать какие переменные в корутине нужно «запомнить» в структуре корутины и вручную реализовывать всю эту механику с их «запоминанием», так, например, безстэковые корутины реализованы в boost::asio, см., например, socket_ и buffer_ в примере https://www.boost.org/doc/libs/1_54_0/doc/html/boost_asio/overview/core/coroutine.html). При этом, поскольку структура для корутины одна, то нельзя сделать как-нибудь так:

coroutine_type coroutine < int a, b; // do something async yield(); >void function() < coroutine_type cor coroutine(); cor.resume(); >await function(); 

Т.е. нельзя ожидать корутины на уровне выше, чем ее вызов, так как у безстэковых корутин тип возвращаемого результата (courotine_type в примере выше) является частью декларации корутины (в отличие от стэковой корутины) и при ее вызове динамически создается структура, содержащая в примере выше a и b, которая одна для всей корутины и не поддерживает стэка вызовов функций/корутин.

В Unity (как и в C++ STL!) именно такие безстэковые корутины. К ним можно прилепить шедулер (например простой цикл для данного трэда, который будет их сам вызывать (см. выше пример с queue и heap)), но у такой реализации ограниченные возможности именно потому, что передать управление «наверх» можно только из самой корутины, а не из промежуточной функции/функций.

Мне кажется, что в случае безстэковых корутин и хитроумного шедулера можно создать deadlock, даже несмотря на то, что трэд один, например трэд делает await cor1 и следом await cor2, обе cor1 и cor2 делают await cor3, которая делает await cor1 и await cor2. Такое вообще возможно? Я в ту сторону думаю? Ну т.е. мой вопрос: имеет ли право на существование шедулер для безстэковых корутин (повторюсь, под шедулером, я имею ввиду не выбор точек переключения (это задает программист вручную вставляя куда нужно yield()), а алгоритм выбора следующей корутины для «возобновления» (resume), например «простой цикл», «цикл с приоритетами» и т.п.).

Стэковые же корутины (они же fiber’ы) позволяют иметь граф вызовов функций, в котором фукнции где-то «внизу» вызывают корутины, а трэд, который «начинает» этот граф, может ждать не только на уровне вызова fiber’ов, но и выше. В такой реализации (стэковые корутины, они же fiber’ы) шедулер имеет смысл. Такие корутины (или правильнее fiber’ы или lightweight threads) реализованы например в boost::coroutine и boost::courotine2 (которые отличаются, например, тем, что в boost::coroutine2 нету симметричных корутин, но при этом обе эти библиотеки реализуют стэковые корутины, они же fiber’ы, они же lightweight threads).

Тот факт, что в C++ корутины безстэковые ограничивает их полезность. Так же, как, например, отсутствие then() у future сильно ограничивает применение future, т.е., например, нельзя написать так (этот пример с future не имеет отношение к корутинам, просто показывает, насколько в C++ то, что появляется — появляется поздно и не доделано по-человечески, как future’ы, так и корутины):

future); f.then([](future &f) < data_type d = f.get(); d.use(); >; do_smth_in_parallel_with_long_process(); 

А придется городить бессмысленный огород как-нибудь так:

future f = async([&f]() < data_type d = long_process(); return d; >); thread([]() < data_type d = f.get(); d.use(); >); do_smth_in_parallel_with_long_process(); 

что проще реализовать вовсе без future:

void callback(data_type &d) < d.use(); >async([&f]() < data_type d = long_process(); callback(d); >); do_smth_in_parallel_with_long_process(); 

что делает future практически бесполезными, так же и то, что корутины в C++ безстэковые тоже делает их использование просто синтактическим сахаром.

Так или иначе шедулер у корутин может быть, как у стэковых, так и у безстэковых (IMHO относительно шедулера у безстэковых корутин, см. вопрос выше про осмысленность шедулера для них и про deadlock), правда во-втором случае смысла в нем мало (IMHO). Стэковые же корутины иначе называются fiber’ами, иначе lightweight thread’ами, иначе, например, goroutine’ами (в Go), отсюда и путаница в терминах. То, что ты называешь трэдами, может быть как полноценными трэдами (или процессами), которые могут выполняться каждый своим CPU, так и fiber’ами, иначе lightweight thread’ами, иначе стэковыми корутинами, которые выполняются в том же трэде (и тем же CPU), но при этом обеспечивают кооперативную многозадачность в точках yield, при этом позволяют иметь шедулер, решающий какую корутину следующую возобновлять при помощи resume(), при этом позволяют иметь любой уровень вложенности функций/корутин) и делают использование шедулера для них чем-то, что имеет смысл.

То, что объединяет безстэковые и стэковые корутины — это то, что они выполняются в том же самом трэде (ну, в отличие, разве что от gotoutines в Go, которые могут быть «перенесены» в другой трэд (см. https://stackoverflow.com/a/19486183 наример), в случае вызова какой-нибудь goroutine’ой блокирующей I/O операции), а значит, как как в случае безстэковых так и стэковых корутин синхронизация (мутексы, семафоры и т.д.) не нужна.

dissident ★★
( 07.03.20 02:30:44 MSK ) автор топика
Последнее исправление: dissident 07.03.20 03:00:17 MSK (всего исправлений: 5)

Ответ на: комментарий от xpahos 06.03.20 20:54:25 MSK

Под полингом я имел в виду не полинг состояния корутин, а полинг состояния абстракций с которыми они работают.

А если они вовсе не работают ни с какими абстракциями как пример Lua выше а просто делают что-то -> отдают управление по yield -> делают что-то -> отдают управление по yield…

Где в этом примере полинг?

Очевидно, что в такой ситуации корутины используют какую-то lock free структуру для обработки cpu bound задач. В такой ситуации не работал с корутинами, не знаю есть ли в этом случае профит.

Зачем корутинам (стэковым или безстэковым) lock-free структуры, если они выполняются в одном трэде?

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

В точках yield()/resume() компилятор должен просто сгенерировать (или программист написать) код, который вызовет что-нибудь вроде swapcontext().

AFAIU так как трэд один, никакой полинг/lock-free структуры/мутексы не нужны.

PS я имею ввиду здесь корутины, которые просто делают yield()/resume() без всяких async операций вроде socket::read() «под ними». Т.е., например, тут трэд один и никакой полинг не нужен:

coroutine_type coroutine() < coroutine_type cor_type; int a, b; while (. ) < do_smth_a_little_bit(); // здесь будет сгенерирован (написан) код а-ля // // longjmp(LABEL ## cor_type.resume_label); yield(); >> // здесь будет сгенерирован (написан) код а-ля // // struct courotine_type < // int a, b // ucontext context; // program_counter pc; // label resume_label; coroutine_type cor1 = coroutine(); coroutine_type cor2 = coroutine(); while (!finished) < // здесь будет сгенерирован (написан) код а-ля // // swapcontext(cor1); // run_starting_from(cor1.pc); // LABEL1: cor1.resume(); // здесь будет сгенерирован (написан) код а-ля // // swapcontext(cor2); // run_starting_from(cor2.pc); // LABEL2: cor2.resume(); >

Выше совсем бредоватый псевдокод, но IMHO смысл какой-то такой.

Если же корутина сама вызывает какую-то async операцию, то там может быть и полинг и мутексы и все что угодно, но это уже не связано с корутиной, это часть этой async операции, как в примере вроде:

coroutine_type coroutine() < data d = await read_from_tcp(); // внутри read_from_tcp() может быть что угодно return d; >

dissident ★★
( 07.03.20 02:53:25 MSK ) автор топика
Последнее исправление: dissident 07.03.20 03:18:15 MSK (всего исправлений: 5)

Ответ на: комментарий от dissident 07.03.20 02:53:25 MSK

В обоих случаях ты прав, но реальность такова, что я не видел еще ни разу cpu bound задач с корутинами, где операции производились бы в одном треде. И соответственно не видел задач, где нужно было бы блокироваться на read, останавливая все корутины.

xpahos ★★★★★
( 07.03.20 06:03:24 MSK )
Ответ на: комментарий от dissident 07.03.20 02:30:44 MSK

ты написал огромный постище, с частью которого я согласен, с частью — нет. напишу просто ту критику, которая возникла при чтении сего.

  1. сначала нвдо сформулировать задачу, которую мы тут пытаемся решить, а то не очень понятно о чем мы тут разговариваем. потому что правильная формулировка задачи, это необходмое условие для ее решения.
  2. отделяй треды от корутин. корутины — это просто такой способ организации псевдопаралельности, и он существует независимо от тредов. у тебя же корутины «выполняются в одном треде». Это сомнительное утверждение, поскольку у тебя могут быть корутины, но тредов может не быть и в помине. ты можешь написать корутиновый кернел для голой железки, и там будут существовать только корутины и никаких тредов и тем более процессов. то есть железка будет стартовать, выполнять код инициализации всяких там внутренностей, передавать управление в main(если это c/c++), а там будут немедля стартовать задачу в виде ну там десятка корутин, и просто ждать когда они все завершатся. Отсюда следует, что корутины существуют вне тредов, и вне компиляторов,и их, как минимум, можно реализовать на си. то есть корутины должны предполагать прежде всего библиотечную реализацию. если для корутин нужна специфическая поддержка компилятора, то это или синтаксический сахар, или не корутины, а что-то особо специфическое. и это особо специфическое обсуждать не стоит вообще.
  3. Лично для меня корутина, это код выполняемый на индивидуальном стеке, с явным переключением этих корутин, причем переключение может быть в любой точке пользовательского кода. То, что в либе, реализующей корутины есть их список, это очевидно, и yield как раз текущую кладет в конец списка, а первую из списка пускает. но это не шедулер. Шедулер это специфическая подсистема, с собственной активностью(работает по прерываниям от таймера, и как бы сам по себе, он работает вне тредов и всего прочего). Код шедулера и код реализации корутин похож, но он работает из разных контекстов. и код реального шедулера сложней, поскольку работает из контекста прерывания. опять же он занимается множеством задач(все зависит от архитектуры кернела), навроде активации кода сработавших системных таймеров, вызов отложенных системных рутин навроде DSR(deferred service routine, в виндовс это называется -deferred procedure call), поддержку приоритетов и все такое.
  4. что касается горутин, то там вроде воркеры — то есть очереди заданий для реальных тредов-воркеров. корутин там нет, поскольку нет явного переключения. впрочем точно не помню. Go не очень интересует.

Остановить coroutine

Скорость падающих предметов (они респаунятся сверху, поэтому переменная static) увеличивается постепенно, но после проигрыша, нажимая restart, скорость не возвращается в изначальное значение, а продолжает увеличиваться, надо как-то остановить куратину и возобновить после рестарта. А while (!Player.lose) не помогает. Можно еще InvokeRepeating использовать вместо куратины, но он так же не останавливается.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
public class Fall : MonoBehaviour { [SerializeField] public static float fallSpeed = 20f; void Start () { StartCoroutine (Accelerate ()); } IEnumerator Accelerate () { while (!Player.lose){ fallSpeed = fallSpeed + 5; yield return new WaitForSeconds (5f); } } void Update () { if (transform.position.y  -34f) Destroy (gameObject); transform.position -= new Vector3 (0, fallSpeed * Time.deltaTime, 0); } }

Лучшие ответы ( 1 )
94731 / 64177 / 26122
Регистрация: 12.04.2006
Сообщений: 116,782
Ответы с готовыми решениями:

Coroutine WaitForSeconds C#
Возникла проблема. На воротах стоят два триггера. При попадании(OnEnter) отключаю второй.

Lerp в Coroutine
Приветствую! Вот кусок кода: while (stepMoveTo < 0.50f) < yield.

Coroutine нетривиальная задача
Здравствуйте форумчане. Расскажу свою задачу потом объясню в чем вопрос. Имеем интерфейс: кнопка.

Как правильно завершить Coroutine?
У меня возник вопрос, как сделать так, чтобы корутин правильно завершал свои действия? У меня он.

Корутины

Сопрограмма позволяет распределять задачи по нескольким фреймам. В Unity сопрограмма — это метод, который может приостановить выполнение и вернуть управление Unity, но затем продолжить с того места, на котором остановился, в следующем кадре.

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

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

Однако важно помнить, что сопрограммы — это не потоки. Синхронные операции, выполняемые внутри сопрограммы, по-прежнему выполняются в основном потоке. Если вы хотите уменьшить количество процессорного времени, затрачиваемого на основной поток, так же важно избегать блокирующих операций в сопрограммах, как и в любом другом коде скрипта. Если вы хотите использовать многопоточный код в Unity, обратите внимание на систему заданий C#.

Сопрограммы лучше всего использовать, если вам нужно иметь дело с длительными асинхронными операциями, такими как ожидание передачи HTTP, загрузки ресурсов или завершения файлового ввода-вывода.

Пример сопрограммы

В качестве примера рассмотрим задачу постепенного уменьшения значения альфы (непрозрачности) объекта, пока он не станет невидимым:

В этом примере метод Fade не дает ожидаемого эффекта. Чтобы сделать затухание видимым, вы должны уменьшить альфа-канал затухания в последовательности кадров, чтобы отобразить промежуточные значения, которые отображает Unity. Однако этот пример метода полностью выполняется в рамках одного обновления кадра. Промежуточные значения никогда не отображаются, и объект мгновенно исчезает.

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

В C# вы объявляете сопрограмму следующим образом:

Сопрограмма — это метод, который вы объявляете с помощью IEnumerator и с выходом оператор возврата включен где-то в теле. Строка yield return null — это точка, в которой выполнение приостанавливается и возобновляется в следующем кадре. Чтобы запустить сопрограмму, вам нужно использовать функцию StartCoroutine:

Счетчик циклов в функции Fade сохраняет свое правильное значение в течение всего времени существования сопрограммы, и любая переменная или параметр сохраняется между операторами yield .

Время задержки сопрограммы

По умолчанию Unity возобновляет сопрограмму во фрейме после оператора yield . Если вы хотите ввести временную задержку, используйте WaitForSeconds:

Вы можете использовать WaitForSeconds для распространения эффекта на определенный период времени, а также в качестве альтернативы включению задач в Обновить метод. Unity вызывает метод Update несколько раз в секунду, поэтому, если вам не нужно, чтобы задача повторялась так часто, вы можете поместить ее в сопрограмму, чтобы получать регулярное обновление. но не каждый кадр.

Например, в вашем приложении может быть будильник, который предупреждает игрока о приближении врага с помощью следующего кода:

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

Это уменьшает количество проверок, которые выполняет Unity, без заметного влияния на игровой процесс.

Остановка сопрограмм

Чтобы остановить сопрограмму, используйте StopCoroutine и Остановить все сопрограммы. Сопрограмма также останавливается, если вы установили для SetActive значение false , чтобы отключить GameObject Фундаментальный объект в сценах Unity, который может представлять персонажей, реквизит, декорации, камеры, путевые точки и многое другое. Функциональность GameObject определяется прикрепленными к нему компонентами. Подробнее
См. в Словарь , к которому прикреплена сопрограмма. Немедленный вызов Destroy(example) (где example – это экземпляр MonoBehaviour ) запускает OnDisable, и Unity обрабатывает сопрограмму, фактически останавливая ее. Наконец, в конце кадра вызывается OnDestroy .

Примечание. Если вы отключили MonoBehaviour , установив enabled значение false , Unity не останавливает сопрограммы.

Анализ сопрограмм

Корутины выполняются иначе, чем код других скриптов. Большая часть кода сценария в Unity отображается в трассировке производительности в одном месте, под определенным вызовом обратного вызова. Однако код ЦП сопрограмм всегда появляется в двух местах трассировки.

Весь исходный код сопрограммы, от начала метода сопрограммы до первого оператора yield , появляется в трассировке каждый раз, когда Unity запускает сопрограмму. Исходный код чаще всего появляется всякий раз, когда вызывается метод StartCoroutine. Сопрограммы, генерируемые обратными вызовами Unity (например, обратные вызовы Start , которые возвращают IEnumerator ), сначала появляются в соответствующем обратном вызове Unity.

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

Это происходит из-за того, как Unity выполняет сопрограммы. компилятор C# автоматически создает экземпляр класса, который поддерживает сопрограммы. Затем Unity использует этот объект для отслеживания состояния сопрограммы при нескольких вызовах одного метода. Поскольку переменные локальной области внутри сопрограммы должны сохраняться при вызовах yield , Unity поднимает переменные локальной области видимости в сгенерированный класс, который остается выделенным в куче во время сопрограммы. Этот объект также отслеживает внутреннее состояние сопрограммы: он запоминает, в какой точке кода сопрограмма должна возобновить работу после завершения.

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

Код, который запускает сопрограмму, создает и вызывает объект, а затем Unity DelayedCallManager снова вызывает его всякий раз, когда сопрограмма yield состояние устраивает. Поскольку сопрограммы обычно запускаются вне других сопрограмм, это распределяет их выполнение между вызовом yield и вызовом DelayedCallManager .

Вы можете использовать окно Unity Profiler , которое поможет вам оптимизировать игру. Он показывает, сколько времени вы тратите на различные области вашей игры. Например, он может сообщать о проценте времени, затраченном на рендеринг, анимацию или игровую логику. Дополнительная информация
См. Словарь , чтобы изучить и понять, где Unity выполняет сопрограммы в вашем приложении. Для этого профилируйте свое приложение с включенным глубоким профилированием, которое профилирует каждую часть кода вашего скрипта и записывает все вызовы функций. Затем вы можете использовать модуль CPU Usage Profiler для исследования сопрограмм в вашем приложении.

Сеанс профилировщика с сопрограммой в DelayedCall

Рекомендуется сократить серию операций до наименьшего возможного количества отдельных сопрограмм. Вложенные сопрограммы полезны для ясности кода и обслуживания, но они требуют больших затрат памяти, поскольку сопрограмма отслеживает объекты.

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

Studhelper IT

Что такое корутины и как с ними работать в Unity. По книге Lucas Faustino “Unity5 Coroutines”.

Эта серия постов для тех, кто интересуется корутинами (сопрограммами), но не очень уверенно использует их в проектах Unity. Требуется базовое знание языка C# и знание интерфейса Unity.

Содержание
1 часть. Что такое корутины. В этой части будет описано, что делает корутина.
2 часть. Coroutine vs Update. Сравним сопрограммы и метод Update и выясним, когда лучше использовать то и другое.
3 часть. Соберем все знания и приведем примеры использования сопрограмм.
4 часть. Финал. Несколько слов, которые не вошли в предыдущие части.

Часть 1. Что такое coroutines?

Coroutine (корутина, сопрограмма) – это выполняемая процедура. Она создается, как и обычный метод, но имеет некоторые полезные функции, которые отличают ее от стандартных методов.

  • ее выполнение можно приостановить. Например, при истечении периода времени или выполнении логического условия.
  • можно использовать бесконечные циклы для создания повторяющихся процедур. Это похоже на метод Update, но имеет свои преимущества в некоторых сценариях.
  • ее полезно использовать при создании машин состояний (например, для искусственного интеллекта).
  • она может быть использована для действий, которые совершаются один раз в определенное время или вообще один раз, таких как открывание двери, движения по локации.

Если вы не привыкли к сопрограммам, то, возможно, думаете, что эти вещи можно сделать с помощью Update. Вы правы, можно. Но корутины позволят это сделать менее грязным и подверженным ошибкам способом.

Создание корутины

Для создания основной корутины нужно сделать два шага. Первый – нужно создать метод, который будет действовать, как корутина. Второй – нам нужно его запустить. Давайте запустим корутину в методе Start скрипта Unity. Наша первая реализация сопрограммы:

using UnityEngine;
using System.Collections;
public class BasicCoroutine : MonoBehaviour
Private void Start()
StartCoroutine(Example());
>
Private IEnumerator Example()
Yield return new WaitForSeconds(1f);
Debug.Log(“We Waited for one second, cool.”):
>
>

Этот пример довольно легко понять. Давайте его разберем. Мы используем встроенный в Unity метод Start(), чтобы сопрограмма запускалась при старте сцены. StartCoroutine – это обязательный метод для запуска. Если вы его не напишете, то сопрограмма работать не будет. Метод StartCoroutine принимает IEnumerator в качестве параметра. Этому требованию отвечает наш метод с названием Example. Если он не будет возвращать IEnumerator, то не подойдет для запуска сопрограммы. В данном примере программа ждет 1 секунду, после чего выполнение корутины прекращается, а в лог выводится запись об ожидании.

Yield

Yield – ключевое слово. В Unity применяется как указание ожидать указанное количество времени. В примере это одна секунда, но есть еще несколько классов, которые могут использованы:

  • WaitForSeconds(float) — пауза на заданное время.
  • WaitForEndOfFrame () — пауза до конца кадра.
  • WaitForFixedUpdate — пауза до следующего фиксированного кадра (стоит подумать о методе FixedUpdate).
  • null — не ждать. Дальше я покажу, где это может быть полезно.
  • break — преждевременный выход. Это похоже на использование break в циклах.

Остановка корутины

Остановка корутины, в основном, означает ранний выход. Она не требуется постоянно, но в некоторых случаях ее нужно знать. Существует два метода для остановки. Метод StopAllCoroutines () остановит все корутины в скрипте. Он нужен, если необходимо убедиться, что все сопрограммы остановлены. Метод StopCoroutine (IEnumerator) остановит определенную сопрограмму.

Первый метод будет выглядеть так:

private void Start()
StartCoroutine(Example());
StopAllCoroutines();
>

Если вы так сделаете, то заметите, что сообщение «We Waited for one second, cool» никогда не выводится на консоль. Это потому, что мы сразу после запуска останавливаем корутину.

private void Stat()
StartCoroutine(Example());
StopCoroutine(Example());
>

Тут происходит то же самое, что и в предыдущем примере. Разница только та, что мы останавливаем одну сопрограмму, а не все.

На базовом уровне это все, что требуется знать для создания сопрограмм в Unity. Дальше поговорим, когда лучше использовать корутины, а не метод Update.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *