LINUX.ORG.RU

unity coroutines or fibers?

 , , ,


0

1

Почитав 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

Я прав?

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

alysnix ()
Ответ на: комментарий от alysnix

Дело в том, что в 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, но контролировать пользователь его не может.

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

PS

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

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

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

есть шедулер и есть весь набор обьектов синхронизации - это треды. в тредах 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 ★★ ()
Ответ на: комментарий от abs

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

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

alysnix ()
Ответ на: комментарий от dissident

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

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

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

alysnix ()
Ответ на: комментарий от dissident

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

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

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

alysnix ()
Ответ на: комментарий от alysnix

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

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

This is impossibly hard to get it right

(c) https://youtu.be/dGVqrGmwOAw?t=1479

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

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

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

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

dissident ★★ ()
Ответ на: комментарий от alysnix

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

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

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

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

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

Опять же если я правильно понял вот это: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4024.pdf

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

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

dissident ★★ ()
Последнее исправление: dissident (всего исправлений: 1)
Ответ на: комментарий от dissident

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

alysnix ()
Ответ на: комментарий от alysnix

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

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

Output MyFunction(Input input) const
{
    Output output(Input.someOtherValue.returnSomething());
    return output;
}

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

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

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

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

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

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

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

  • Прав ли я, что корутины в Unity - это fibers?
  • Правильно ли я понял эти определения и примеры вообще?
dissident ★★ ()
Последнее исправление: dissident (всего исправлений: 5)
Ответ на: комментарий от dissident

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

alysnix ()
Ответ на: комментарий от alysnix

Я не уверен, что они бесстековые (см. ниже примеры с 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 ★★ ()
Последнее исправление: dissident (всего исправлений: 3)
Ответ на: комментарий от alysnix

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

Да, в 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 ★★ ()
Ответ на: комментарий от menangen

Не очень только понятно, чем это всё отличается в концепции от 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 ★★ ()
Последнее исправление: dissident (всего исправлений: 3)
Ответ на: комментарий от dissident

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

alysnix ()
Ответ на: комментарий от dissident

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

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

alysnix ()
Ответ на: комментарий от alysnix

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

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

xpahos ★★★★★ ()
Ответ на: комментарий от xpahos

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

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

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

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

alysnix ()
Ответ на: комментарий от alysnix

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

xpahos ★★★★★ ()
Ответ на: комментарий от alysnix

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

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

void Coroutine()
{
    while (!Poll())
    {
        yield();
    }
    DoSmth();

}

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

void NonBlockingFunctionUsingCoroutineRead()
{
    response = await ReadFromTcp();
    DoSmth(response);
}

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

do_smth((function() {
  do_smth_else(function() {
     do_yet_another_thing();
  });
});

do() {
   do_smth();
}

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

do() {
   await do_smth();
   await do_smth_else();
   do_yet_another_thing();
}

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

void Coroutine1()
{
    while (true)
    {
        DoSmth1();
        yield();
    }
}

void Coroutine2()
{
    while (true)
    {
        DoSmth2();
        yield();
    }
}

Coroutine1 cor1;
Coroutine2 cor2;

while (true)
{
   cor1.resume();
   cor2.resume();
}

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

while (true)
{
   DoSmth1();
   DoSmth2();
}

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

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

void Coroutine1()
{
    // same as above
}

void Coroutine2()
{
    // same as above
}

SetSomeKindOfManagerWithPrioritiesForExampleAndRun()
{    
    Coroutine1 cor1;
    Coroutine2 cor2;
    cor1.SetPiority(1);
    cor1.SetPiority(2);
    cor1();
    cor2();
}

Т.е. и корутины и волокна - это просто возможность написать однотрэдовый код, в котором передается управление между корутинами/волокнами в точках, заданных пользователем (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()
{
   DoSmthLong();
   yield();
}

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

dissident ★★ ()
Последнее исправление: dissident (всего исправлений: 1)
Ответ на: комментарий от xpahos

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

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

    -- 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 ★★ ()
Ответ на: комментарий от dissident

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

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

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

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

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

alysnix ()
Ответ на: комментарий от xpahos

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

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

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

dissident ★★ ()
Ответ на: комментарий от alysnix

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

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

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

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

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

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

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

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

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

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

dissident ★★ ()
Последнее исправление: dissident (всего исправлений: 2)
Ответ на: комментарий от dissident

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

alysnix ()
Ответ на: комментарий от alysnix

я посмотрел по диагонали

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

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

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

queue<function<void(int, int)>> queue;

void coroutine(int id,
               int count,
               function<void(int,int)> next = function<void(int,int)>())
{
    while (count--)
    {
       cout << "id: " << id << ", count: " << count << endl;
       co_yield();
    }

    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 ★★ ()
Последнее исправление: dissident (всего исправлений: 8)
Ответ на: комментарий от dissident

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

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

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

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

alysnix ()
Ответ на: комментарий от dissident

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

class Coroutine{

void run()=0; //тело корутины

void start(); //запуск корутины

//два способа переключения

static void yield(); //переключение на следующую корутину в списке активных корутин

static void yield(Coroutine *to); переключение на именно указанную

}

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

alysnix ()
Ответ на: комментарий от alysnix

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

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

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

alysnix ()
Ответ на: комментарий от alysnix

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

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

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

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

dissident ★★ ()
Ответ на: комментарий от alysnix

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

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

dissident ★★ ()
Последнее исправление: dissident (всего исправлений: 2)
Ответ на: комментарий от dissident

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

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

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

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

dissident ★★ ()
Ответ на: комментарий от dissident

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

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

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

alysnix ()
Ответ на: комментарий от dissident

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

xpahos ★★★★★ ()
Ответ на: комментарий от alysnix

у корутин нет шедулера,

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

У 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 трэду, а в случае симметричных они переключаются между друг другом.

(с) https://stackoverflow.com/a/42042904

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

queue<fiber_type> 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<fiber_type> 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<data_type f = async([]() {
    data_type d = long_process();
    return d;
});

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

do_smth_in_parallel_with_long_process();

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

future<data_type> 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 ★★ ()
Последнее исправление: dissident (всего исправлений: 5)
Ответ на: комментарий от xpahos

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

А если они вовсе не работают ни с какими абстракциями как пример 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; <- это совсем псевдокод, я не очень силен в asm
//     ...
// }
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 ★★ ()
Последнее исправление: dissident (всего исправлений: 5)
Ответ на: комментарий от dissident

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

xpahos ★★★★★ ()
Ответ на: комментарий от dissident

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

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

  2. отделяй треды от корутин. корутины - это просто такой способ организации псевдопаралельности, и он существует независимо от тредов. у тебя же корутины «выполняются в одном треде». Это сомнительное утверждение, поскольку у тебя могут быть корутины, но тредов может не быть и в помине. ты можешь написать корутиновый кернел для голой железки, и там будут существовать только корутины и никаких тредов и тем более процессов. то есть железка будет стартовать, выполнять код инициализации всяких там внутренностей, передавать управление в main(если это c/c++), а там будут немедля стартовать задачу в виде ну там десятка корутин, и просто ждать когда они все завершатся. Отсюда следует, что корутины существуют вне тредов, и вне компиляторов,и их, как минимум, можно реализовать на си. то есть корутины должны предполагать прежде всего библиотечную реализацию. если для корутин нужна специфическая поддержка компилятора, то это или синтаксический сахар, или не корутины, а что-то особо специфическое. и это особо специфическое обсуждать не стоит вообще.

  3. Лично для меня корутина, это код выполняемый на индивидуальном стеке, с явным переключением этих корутин, причем переключение может быть в любой точке пользовательского кода. То, что в либе, реализующей корутины есть их список, это очевидно, и yield как раз текущую кладет в конец списка, а первую из списка пускает. но это не шедулер. Шедулер это специфическая подсистема, с собственной активностью(работает по прерываниям от таймера, и как бы сам по себе, он работает вне тредов и всего прочего). Код шедулера и код реализации корутин похож, но он работает из разных контекстов. и код реального шедулера сложней, поскольку работает из контекста прерывания. опять же он занимается множеством задач(все зависит от архитектуры кернела), навроде активации кода сработавших системных таймеров, вызов отложенных системных рутин навроде DSR(deferred service routine, в виндовс это называется -deferred procedure call), поддержку приоритетов и все такое.

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

alysnix ()