Подтвердить что ты не робот

Как стекированные сопрограммы отличаются от штабельных сопрограмм?

Фон:

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

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

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

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

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

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

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

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

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

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

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

4b9b3361

Ответ 1

Во-первых, спасибо, что посмотрели CO2:)

Boost.Coroutine doc описывает преимущество многоуровневой справки:

stackfulness

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

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

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

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

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

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

Вы хотите преобразовать его в итератор, со стеквой сопрограммой, вы можете:

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

Но с помощью стекированной сопрограммы нет возможности сделать это:

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

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

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

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

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

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

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

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

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

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.

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

Нет.

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

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

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

Ответ 2

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

Вы можете использовать что-то вроде boost.fiber, которое реализует потоки/волокна пользовательской земли на основе boost.context.