Чем stackless coroutines отличаются от stackful coroutines?

Справочная информация:

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

мой предлагаемое решение:

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

детали реализации:

думая о том, как я бы это сделал, у меня возникают проблемы с пониманием функциональных различий между stackless и stackful coroutines. У меня есть некоторый опыт использования stackful coroutines с помощью импульс.Корутина!--18--> библиотека. Я считаю, что это относительно легко понять с концептуального уровня: для каждой подпрограммы он поддерживает копию контекста ЦП и стека, и когда вы переключаетесь на сопрограмму, он переключается на этот сохраненный контекст (так же, как планировщик режима ядра).

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

поэтому я хотел бы более четко понять функциональные различия между stackless и stackful coroutines. В частности, я думаю об этих вопросах:

  • ссылки, как этот предположим, что различие заключается в том, где вы можете дать/возобновить в stackful против stackless coroutine. Это так? Есть простой пример того, что я могу сделать в stackful сопрограмма, но не в stackless один?

  • существуют ли какие-либо ограничения на использование автоматических переменных хранения (т. е. переменных "в стеке")?

  • существуют ли какие-либо ограничения на то, какие функции я могу вызвать из бесконтактной корутины?

  • Если нет сохранения контекста стека для Stackless coroutine, куда идут переменные автоматического хранения, когда coroutine бежит?

2 ответов


во-первых, спасибо, что взглянули на CO2 :)

Импульс.Корутина!--10-->doc описывает преимущество stackful coroutine хорошо:

stackfulness

в отличие от stackless coroutine в stackful сопрограмма может быть приостановлен из вложенного stackframe. Исполнение возобновляется на точно такая же точка в коде, где она была приостановлена раньше. С в stackless coroutine, только рутина верхнего уровня может быть приостановлена. Любая процедура, вызванная этой процедурой верхнего уровня, сама по себе не может приостановиться. Это запрещает предоставлять операции приостановки / возобновления в подпрограммах внутри библиотека общего назначения.

первый-класс продолжение

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

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

что это значит для вас? например, представьте, что у вас есть функция, которая принимает посетителя:

template<class Visitor>
void f(Visitor& v);

вы хотите преобразовать его в итератор, с помощью stackful coroutine, вы может:

asymmetric_coroutine<T>::pull_type pull_from([](asymmetric_coroutine<T>::push_type& yield)
{
    f(yield);
});

но с stackless coroutine, нет никакого способа сделать это:

generator<T> pull_from()
{
    // yield can only be used here, cannot pass to f
    f(???);
}

В общем, stackful coroutine является более мощным, чем stackless coroutine. Так почему мы хотим stackless coroutine? краткий ответ: эффективность.

Stackful coroutine обычно необходимо выделить определенный объем памяти для размещения своего стека времени выполнения (должен быть достаточно большим), а контекстный переключатель дороже по сравнению с бесконтактным, например Boost.Coroutine принимает 40 циклов, в то время как CO2 занимает всего 7 циклов в среднем на моей машине, потому что единственное, что нужно восстановить stackless coroutine, - это счетчик программ.

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

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

ответить на вопросы:

  • существуют ли какие-либо ограничения на использование автоматических переменных хранения (т. е. переменные "в стеке")?

нет. Это ограничение эмуляции CO2. С поддержкой языка, автоматические переменные хранения виден корутину будет помещено на внутреннем запоминающем устройстве coroutine. Обратите внимание на мой акцент на "видимый для coroutine", если coroutine вызывает функцию, которая использует переменные автоматического хранения внутри, то эти переменные будут помещены в стек выполнения. Более конкретно, stackless coroutine только должен сохранить переменные/временные значения, которые можно использовать после возобновления.

чтобы быть ясным, вы можете использовать автоматические переменные хранения в теле корутина CO2:

auto f() CO2_RET(co2::task<>, ())
{
    int a = 1; // not ok
    CO2_AWAIT(co2::suspend_always{});
    {
        int b = 2; // ok
        doSomething(b);
    }
    CO2_AWAIT(co2::suspend_always{});
    int c = 3; // ok
    doSomething(c);
} CO2_END

до тех пор, пока определение не предшествует любому await.

  • есть ли какие-либо ограничения на то, какие функции я могу вызвать из stackless сопрограмма?

нет.

  • если нет сохранения контекста стека для бессекторной корутины, где делать автоматические переменные хранения идут когда coroutine бежать?

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

если у вас есть какие-либо сомнения, просто проверьте исходный код СО2, это может помочь вам понять механику под капотом ;)


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

вы можете использовать что-то вроде boost.волокно которое снабжает потоки/волокна потребител-земли основанные на подталкивании.контекст.