В последнее время из открытых проектов крупных компаний все более популярной становится высокопроизводительная серверная разработка на основе сопрограмм, например, я узнал, что команда WeChatlibco, Мейзуlibgo,а такжеlibcopp. Разработка традиционных высокопроизводительных серверов в основном основана на асинхронных фреймворках и многопоточных или многопроцессных моделях.Хотя эта архитектура прошла длительные испытания и испытана, ей присущи недостатки:
(1).
Асинхронная архитектура принудительно разделяет логику кода, что не способствует привычкам последовательного мышления человеческого здравого смысла и, естественно, недружелюбно для разработчиков;
(2).Хотя потоки совместно используют большой объем данных по сравнению с процессами, эффективность создания и переключения высока.Они рассматриваются как легкие планировщики на уровне ядра.В архитектуре X86 для переключения потоков требуется большое количество Циклы инструкций ЦП для завершения; в то же время, когда бизнес растет, если вычислительная мощность увеличивается за счет увеличения количества рабочих потоков, можно потреблять большую часть ресурсов системы в накладных расходах ресурсов управления потоками и планирования потоков. , и добиться обратного эффекта.Поэтому количество рабочих процессов в Nginx такое же, как и количество исполняющих блоков ЦП.За счет родства процесса (потока) с ядром ЦП потери, вызванные переключением процесса (потока) (такие как как инвалидация кеша и т. д.) можно свести к минимуму;
(3).
Хотя иногда мы можем отказаться от планирования запросов ЦП с помощью ::sched_yield();, подключаемый поток полностью определяется алгоритмом планирования, который является пассивным по отношению к включенному потоку, как обычный поток производства-потребления. модели, они могут только пассивно сосуществовать, и трудно добиться эффективного «сотрудничества»;
(4) Также по вышеуказанным причинам переключение между потоками в основном является пассивным состоянием, которое не контролируется пользовательской программой, поэтому многие критические разделы должны быть явно защищены блокировкой.
В этой среде появилась более легкая разработка сопрограмм, которая широко используется крупными производителями. В дополнение к высокопроизводительным библиотекам сопрограмм, разработанным крупными научно-исследовательскими компаниями, которые обслуживают собственный бизнес, библиотека Boost также выпустила библиотеку сопрограмм Boost.Coroutine2, которая включает в себя пакеты сопрограмм без стека и со стеком. мой предыдущий"Использование Coroutine Coroutines в Boost.Asio” уже сделал относительно подробное введение. Здесь мы в основном узнаем о базовых вещах, таких как управление ресурсами и обслуживание в процессе переключения сопрограмм на более низком уровне по сравнению с высокоуровневым интерфейсом сопрограмм — библиотекой Boost.Context (применимо к стековым сопрограммам).
На самом деле существует много способов реализации сопрограмм. Способные производители могут вручную создавать и поддерживать пространство стека, сохранять и переключать состояние выполнения регистров ЦП и другую информацию. Эти методы тесно связаны с архитектурой и требуют большего количества сборок. быстро разрабатывают прототипы сопрограмм, они обычно используют существующие инструменты, такие как ucontext или Boost.Context, чтобы помочь в управлении пространством стека и рабочим статусом.ucontext имеет долгую историю и хранится в структуре ucontext_t.Информация о стеке, контекст выполнения ЦП, сигнал mask и адрес следующей структуры ucontext_t, необходимой для возобновления работы, но производительность ucontext, измеренная другими, намного медленнее, чем у Boost.Context. Boost.Context — это базовая поддержка основных сопрограмм C++ в этом году. библиотека, производительность была оптимизирована.
Работа, выполняемая Boost.Context, заключается в сохранении абстрактной информации о состоянии текущего выполнения (пространство стека, указатель стека, регистр ЦП и регистр состояния, указатель IP-инструкции) в традиционной среде потока, а затем приостановка текущего состояния выполнения, Поток выполнения программы переходит к другим местам для продолжения выполнения Эта базовая конструкция может использоваться для открытия потоков пользовательского режима для создания более продвинутых операционных интерфейсов, таких как сопрограммы. В то же время, поскольку этот переключатель находится в пользовательском пространстве, потребление ресурсов очень мало, и вся информация о пространстве стека и состоянии выполнения сохраняется одновременно, поэтому функции в нем могут свободно вкладываться.
Из информации, которую я проверил, недавно выпущенная новая версия Boost.Context была значительно обновлена по сравнению со старой версией, абстрагируя тип execute_context, Из его внутреннего файла реализации видно, что внутренняя инфраструктура все еще использует fcontext_t. Сохраните состояние, используйте make_fcontext, jump_fcontext и новый ontop_fcontext для управления им, знакомым с предыдущими версиями.большие парниКонечно, эти интерфейсы можно вызывать напрямую. Теперь последний Boost.Context опирается на некоторые новые функции C ++ 11, а библиотека сопрограмм Boost также поддерживает две версии Boost.Coroutine и Boost.Coroutine 2. Я не знаю, является ли это причиной, в конце концов, их авторы Оливер
Ковальке.
При создании контекста выполнения сначала будет выделено пространство стека контекста, и сохранена структура данных, которая поддерживает информацию о контексте в верхней части стека. Эта структура данных не может быть доступна в среде выполнения_контекста в дизайне, только когда оператор () Статус будет автоматически обновляться и сохраняться, и пользователю не нужно заботиться об этом. Так же, как boost::thread, operator()execution_context не поддерживает копирование, поддерживаются только операции построения перемещения и операции присваивания перемещения.
Для всего execute_context требуется контекстная функция, чья сигнатура функции выглядит следующим образом:
auto execution_context(execution_context ctx, Args ... args)
Первый параметр ctx фиксированный, что указывает на то, что он будет автоматически переключать возобновление в контекст, когда текущий контекст приостанавливается, обычно это создатель и вызывающий объект текущего контекста, а следующие переменные параметры будут автоматически переданы в execute_context::operator() функционировать как параметр.
Пример простого использования execute_context Boost.Context
int n = 9;
ctx::execution_context<int> source(
[n](ctx::execution_context<int> sink, int/*not used*/ ) mutable {
int a=0, b=1;
while(n-- >0){
auto result = sink(a);
sink = std::move(std::get<0>(result));
auto next = a + b;
a = b; b = next;
}
return sink;
});
for(int i=0; i<10; ++i) {
if(source) {
auto result = source(0);
source = std::move( std::get<0>(result) );
std::cout << std::get<1>(result) << " ";
}
}
// 输出结果为:0 1 1 2 3 5 8 13 21 0 %
Тип возвращаемого значения функции связан с типом параметра шаблона экземпляра execute_context: если нет необходимости в передаче данных между контекстами приостановки и возобновления, а только переключение потока управления, вы можете использовать void для создания экземпляра типа execute_context для создать объект, в противном случае для возобновления Скажите, что возвращаемое значение, которое он получает, имеет тип std::tuple, первое значение является объектом контекста приостановки, а остальные являются упакованными возвращаемыми значениями.Если возвращается только одно значение, но другое типы данных, вы можете рассмотреть возможность использования boost: :variant, несколько возвращаемых значений могут быть инкапсулированы по очереди
ctx::execution_context<int, std::string> ctx1(
[](ctx::execution_context<int, std::string> ctx2, int num, std::string) {
std::string str;
std::tie(ctx2, num, str) = ctx2(num+9, "桃子是大神");
return std::move(ctx2);
});
int i = 1;
int ret_j; std::string ret_str;
std::tie(ctx1, ret_j, ret_str) = ctx1(i, "");
std::cout << ret_j << "~" << ret_str << std::endl;
Если вы хотите дополнительно выполнить другую функцию, указанную вами в возобновленном контексте, вы можете установить первый параметр вызова в exec_ontop_arg, затем следовать вызываемой функции, а затем передать функцию, которую необходимо передать контекстом в обычном режиме. . , При вызове параметры передаются указанной функции для выполнения, и тип возвращаемого значения этой функции должен быть параметром, инкапсулированным std::tuple, который может быть передан в контекст возобновления, а затем происходит переключение контекста и возобновление использует свои параметры для продолжения выполнения. Вскоре это было представлено в новой версии Boost.Context, и эффект эквивалентен добавлению вызова перехватчика в исходный контекст выполнения.
ctx::execution_context<int> func1(ctx::execution_context<int> ctx, int data) {
std::cout << "func1: entered first time: " << data << std::endl;
std::tie(ctx, data) = ctx(data+1);
std::cout << "func1: entered second time: " << data << std::endl;
std::tie(ctx, data) = ctx(data+1);
std::cout << "func1: entered third time(atten): " << data << std::endl;
return ctx;
}
std::tuple<boost::context::execution_context<int>,int> func2(boost::context::execution_context<int> ctx, int data)
{
std::cout << "func2: entered: " << data << std::endl;
return std::make_tuple(std::move(ctx), -3);
}
int main(int argc, char* argv[]){
int data = 0;
ctx::execution_context< int > ctx(func1);
std::tie(ctx, data) = ctx(data+1);
std::cout << "func1: returned first time: " << data << std::endl;
std::tie(ctx, data) = ctx(data+1);
std::cout << "func1: returned second time: " << data << std::endl;
std::tie(ctx, data) = ctx(ctx::exec_ontop_arg, func2, data+1);
return 0;
}
Результат вышеприведенного вывода кода показан ниже, data+1==5 передается в func2, затем func2 оборачивает ctx и свои собственные параметры, ctx продолжает выполняться, используя параметры, переданные в func2:
func1: entered first time: 1
func1: returned first time: 2
func1: entered second time: 3
func1: returned second time: 4
func2: entered: 5
func1: entered third time(atten): -3
Объект execute_context будет выделять стек контекста при его создании и будет автоматически уничтожен, когда функция контекста вернется.
После расследования было обнаружено, что execute_context был введен в Boost-1.59.В предыдущей версии структура fcontext_t напрямую управлялась совместным вызовом jump_fcontext() и make_fcontext() для сохранения и переключения стека и информации о состоянии выполнения, хотя сейчас execute_context encapsulated Он проще в использовании, но старомодная структура операции fcontext_t легче для понимания.Если вы заинтересованы в более подробном изучении, вы можете обратиться к старой версии документации.
Когда я раньше смотрел на Boost.Coroutine, что такое call_type, push_type... концепция была ошеломляющей. Здесь я смотрю на базовую структуру Boost.Context, поддерживаемую нижним уровнем сопрограммы. После вещей, которые зависят от базовой архитектуры, инкапсулированный, я могу с нетерпением ждать того дня, когда у меня будет время сделать свою собственную библиотеку сопрограмм После изучения принципов и идей библиотек сопрограмм, таких как libgo и libcopp, я тоже не буду делать колесо!
PS: На самом деле, сопрограмма сделала мышление пользователя синхронным, поэтому человек, который разрабатывает библиотеку сопрограмм, должен взять на себя задачу скачка потока выполнения. Приведенный выше пример не должен быть особенно сложным для понимания, Boost::Context предусматривает, что когда вызывается execute_context::operator(), работа программы переключается, так что указанная выше точка переключения также понятна. Просто чувствуется, что изменения в недавно вышедших версиях слишком велики.Хотя названия одинаковые,иногда это указатель,а иногда и не указатель.Лучше указать версию - 1.62.0.