Здравствуйте, ksandro, Вы писали:
K>Есть высоконагруженный сервер. К нему каждую секунду поступает 100500 запросов. Если на каждый запрос создавать поток, а затем еще создавать поток для запроса к БД, то сервер будет тратить большую часть ресурсов исключительно на создание/убийство потоков, а так же на их синхронизацию, при этом потоки большую часть времени будут ничего не делать а просто спать ожидая ответа от бд, в итоге мы имеем очень низкую производительность трату огромных ресурсов на ничего не делание. Поэтому уже давно высокопроизводительные сервера работают несколько подругому. На самом деле с сетью работает ядро, да и с дисками работает ядро, и есть специальные системные вызовы, сигнализирующие нам о том, что какой-то в какой-то файл/сокет доступен для чтения/записи (epoll, IOCP). Да нам нужно что-то типа планировщика. В обычной ситуации мы сохраняем контекст запроса, затем можем ожидать или отправлять следующие запросы. Когда пришел ответ, мы вызываем некий коллбэк. Корутина отправляет запрос и возвращается, когда приходит ответ мы в качестве коллбека вызываем ту же самую корутину, и она продолжает работату с того самого места, где мы отправили запрос. То есть внутри корутины все выглядит как синхронная процедура, после запроса мы получаем ответ, и продолжаем работу. K>Описал я как-то сумбурно, но вообще это сложно объяснить в двух словах. посмотрите например на примеры работы библиотеки boost asio (https://www.boost.org/doc/libs/latest/doc/html/boost_asio/examples/cpp20_examples.html#boost_asio.examples.cpp20_examples.coroutines) код с корутинами выглядит красивее. Ну и вообще погуглите про написание сетевых программ на базе корутин. Код с корутинами по скорости работает так же как неблокирующий сервер на коллбеках, накладные расходы минимальны, не сравнить с порождением потока на каждый чих. Есть еще стекфул корутины и файберы, тоже интересная штука.
Правильно ли я понимаю, что если в качестве обработчика асинхронного ответа использовать лямбду, которую положить контекст и которую через технологию Signal-Slot соединить с обработчиком, то будет тоже самое?
Здравствуйте, kov_serg, Вы писали:
_>Остальное фантики. loop-fn.h — это реализация stackless корутин на plainC. Не наравится можно писать как больше нравится. На общий подход не влияет.
Здравствуйте, kov_serg, Вы писали:
L>>Господа, а что вы вкладываете в понятие "отладка (кооперативной многозадачности|многопоточности)" и почему это такая большая проблема? Для друга интересуюсь. _>Очень просто потому как многозадачность в c++ не структурированая. Поэтому и отладка превращается в: поймай угря в ведре с угрями.
А можно с примерами? Именно отладки, а не борьбы с гонками?
Здравствуйте, Marty, Вы писали:
M>Здравствуйте, kov_serg, Вы писали:
_>>Остальное фантики. loop-fn.h — это реализация stackless корутин на plainC. Не наравится можно писать как больше нравится. На общий подход не влияет.
M>А можно ссылку на топик?
На какой топик? Там файл из 20 строк.
/* loop-fn.h */#ifndef __LOOP_FN_H__
#define __LOOP_FN_H__
typedef int loop_t;
#define LOOP_RESET(loop) { loop=0; }
#if defined(__COUNTER__) && __COUNTER__!=__COUNTER__
#define LOOP_BEGIN(loop) { enum { __loop_base=__COUNTER__ }; \
loop_t *__loop=&(loop); __loop_switch: int __loop_rv=1; \
switch(*__loop) { default: *__loop=0; case 0: {
#define LOOP_POINT { enum { __loop_case=__COUNTER__-__loop_base }; \
*__loop=__loop_case; goto __loop_leave; case __loop_case:{} }
#else
#define LOOP_BEGIN(loop) {loop_t*__loop=&(loop);__loop_switch:int __loop_rv=1;\
switch(*__loop){ default: case 0: *__loop=__LINE__; case __LINE__:{
#define LOOP_POINT { *__loop=__LINE__; goto __loop_leave; case __LINE__:{} }
#endif
#define LOOP_END { __loop_end: *__loop=-1; case -1: return 0; \
{ goto __loop_end; goto __loop_switch; /* make msvc happy */ } } \
}} __loop_leave: return __loop_rv; }
#define LOOP_SET_RV(rv) { __loop_rv=(rv); } /* rv must be non zero */#define LOOP_INT(n) { __loop_rv=(n); LOOP_POINT } /* interrupt n */
/* for manual labeling: enum { L01=1,L02,L03,L04 }; ... LOOP_POINT_(L02) */#define LOOP_POINT_(name) { *__loop=name; goto __loop_leave; case name:{} }
#define LOOP_INT_(n,name) { __loop_rv=(n); LOOP_POINT_(name) }
#endif/* __LOOP_FN_H__ */
M>Чем оно лучше, чем Protothreads?
Где написано что лучше. Оно просто позволяет писать постепенно исполняемые функции на любых C-шных компиляторах в стиле coroutines.
Здравствуйте, kov_serg, Вы писали:
_>>>Остальное фантики. loop-fn.h — это реализация stackless корутин на plainC. Не наравится можно писать как больше нравится. На общий подход не влияет.
M>>А можно ссылку на топик?
_>На какой топик? Там файл из 20 строк. _>
_>/* loop-fn.h */
_>
А есть примеры, как это использовать? А то самому в этой мешанине разбираться неохота
M>>Чем оно лучше, чем Protothreads?
_>Где написано что лучше. Оно просто позволяет писать постепенно исполняемые функции на любых C-шных компиляторах в стиле coroutines.
Protothreads позволяет тоже самое. Зачем было написано это?
Здравствуйте, landerhigh, Вы писали:
L>Здравствуйте, kov_serg, Вы писали:
L>>>Господа, а что вы вкладываете в понятие "отладка (кооперативной многозадачности|многопоточности)" и почему это такая большая проблема? Для друга интересуюсь. _>>Очень просто потому как многозадачность в c++ не структурированая. Поэтому и отладка превращается в: поймай угря в ведре с угрями.
L>А можно с примерами? Именно отладки, а не борьбы с гонками?
Простой пример вы вызвали функцию, она породила поток, который породил еще несколько потоков.
1. вы не знаете сколько потоков было порождено (да и сколько сейчас работает)
2. в случае убийства потока, дочерние потоки не остановятся.
3. если остановка поток требует специальной очастки типа at_thread_exits реализуется костылями (всякие ssl,tls)
4. вы не можете приостановить поток (в винде впринципе можно, но в общем случае нет)
5. вы не можете явно отслеживать переключение потоков (только косвенно).
Обычная отладка это анализ логов.
Здравствуйте, Marty, Вы писали:
M>А есть примеры, как это использовать? А то самому в этой мешанине разбираться неохота
Очень просто это просто функция. Она имеет стосояние и выполняет задачу постепенно по мере передачи ей управления. О завершении сообщает с помощью кода возврата.
Код примерно такой:
Только с расширением для кода возврата можно прерываться не с 1, а с любым кодом с помошью LOOP_INT(interrupt_code)
Код 0 означает что функция завершила работу, 1 еще работает остальные планировшик может использовать для наращивания функционала.
В частности для задержек по времени или обработки запросов от функции
Любая асинхронная функция может получать события (event) через метод setup и производить работу когда ей явно передадут упраление через loop()
При этом для инициализации используется sevInit а для освобождения ресурсов событие sevDone. Код возврата 0 — значит успешно, остальное нет
Гарантируется что если был вызван sevInit то потом будет вызван sevDone.
После sevInit можно вызывать loop который сообщит что он закончил если вернёт 0. Он выполняет задачу порциями так чтобы не зависать на долго.
Если возвращает 1 то он еще работает и требует еще кванта выполнения, остальные коды возврата зависят от планировщика.
Например числа <0 можно использовать для sleep(timeout) а коды 2,3,4... для других прерываний/запросов
Например фунцкия может сообщить что очередная порция данных готова, или запросить данные из внешних источников и только после их получений или timeout-та возобновить исполнение.
Сами планировщики отдельно. _>>Где написано что лучше. Оно просто позволяет писать постепенно исполняемые функции на любых C-шных компиляторах в стиле coroutines. M>Protothreads позволяет тоже самое. Зачем было написано это?
protothread и loop-fn.h похожи но решают разные совершенно задачи.
afn_t нужна для декомпозиции асинхронных задач и получения структурированной конкуренции, а так же для возможности сохранять состояния постепенно исполняемых функций.
Но в основном задача была сделать максимально просто. loop-fn просто побочный файл позволяющий писать код в стиле корутин на любом C компиляторе для максимального охвата.
Здравствуйте, so5team, Вы писали:
S>А зачем? S>Прелесть stackfull-короутин в том, что ты пишешь обычный код (как при классическом многопоточном программировании).
даже проще чем в многопоточном, в большинстве случаев можно не париться о синхронизации.
S>А моменты переключения делаются автоматически под капотом функций, которые могут заблокировать выполнение (например, read/write/accept/listen и т.п.).
Да, тут согласен. Просто мой небольшой опыт использования stackfull корутин на практике показал, что оберток над всеми функциями ввода вывода, которые нужны нету. Пришлось самому писать эти обертки, вызывающие yield(). А это не всегда так просто как кажется на первый взгляд. То есть я довольно много времени потратил на написание кода специфичного именно для корутин. Но да, вя бизнес логика уместиласть в одну функцию, и ее можно было даже для отладки запускаать как синхронную блокирующую.
Здравствуйте, kov_serg, Вы писали:
L>>А можно с примерами? Именно отладки, а не борьбы с гонками? _>Простой пример вы вызвали функцию, она породила поток, который породил еще несколько потоков. _>1. вы не знаете сколько потоков было порождено (да и сколько сейчас работает)
обычно таки знаем. Более того, в нашей тихой гавани нужно точно знать, сколько всего есть потоков. Потому что isolation, affinity и прочие страшные слова.
_>2. в случае убийства потока, дочерние потоки не остановятся.
Стараемся рисовать такую архитектуру, где удается обойтись без геноцида работающих потоков. А если поток сам роскомнадзорнулся, то это уже авария. Самый правильный вариант — let it crash, и желательно прям сразу, чтобы адекватный coredump получить.
_>3. если остановка поток требует специальной очастки типа at_thread_exits реализуется костылями (всякие ssl,tls)
Бывает. Решается.
_>4. вы не можете приостановить поток (в винде впринципе можно, но в общем случае нет)
Вот ни разу не было необходимости.
_>5. вы не можете явно отслеживать переключение потоков (только косвенно).
И этого тоже не требовалось.
Это задача системных тулзов и заморачиваться этим есть смысл, только если оправдано заниматься привязкой рабочих потоков к ядру.
_>Обычная отладка это анализ логов.
Здравствуйте, LaptevVV, Вы писали:
LVV>А в каких задачах корутины вот прям супер — супер? LVV>Чего раньше приходилось делать муторно и долго ?
Ни в каких. Они могут быть удобны в задачах где нужны конечные автоматы
или событийное программирование. Они позволяют писать как будто бы императивный
код. Проблема в том, что в конечном счёте для разработки автоматы -- лучше.
Проблема импертивного программирования в задачах логического управления,
чтоб собственно логика не проектируется отдельно от программирования (кодирования),
а рождается программистом по ходу дела из головы. И в любом более-менее сложном
случае реализуемая логическая функция оказывается не полноценной (логические ошибки).
Для автоматов можно хотя бы выполнить простейшие проверки, что все предусмотренные
состояния достижимы, что нет тупиковых состояний из которых нет выхода, что
во всех возможных состояниях предусмотрена реакция на все возможные входные события.
Можно с помощью систем проверки моделей проверить, что реализуемый конечный автомат
удовлетворяет заданным проверочным условиям во всём множестве его возможных состояний
и в любой комбинации поступающих входных событий. Это дорогого стоит. Альтернатива
в виде тестирования лишь покрывает узкий набор сценариев.
Простейшая иллюстрация того, о чём я говорю, многие функции ожидания событий
в императивном программировании ожидают ровно ОДНО событие (иначе при вызове
такой функции расписывать switch-case на два десятка вариантов...) И такие
алгоритмы прекрасно работают когда события происходят в определённом, предусмотренном
прогрммистом порядке. А что если порядок не детерминированный (а программист
этого не предусмотрел). Событийное программирование снимает этот недостаток,
но привносит описанные выше проблемы: состояние теперь не выделено явным образом
(в императивной программе оно определяется счётчиком программных инструкций),
и программист вообще затрудняется сказать в скольки состояниях может находиться
программа и в скольки из них какие события и как обрабатываются. Состояние
закодировано в очередях сообщений, в факте подписки на определённые события,
в отдельных переменных. Его можно выделить в отдельную единую переменную и тогда
получится ДКА, с которым легко работать. А как его программировать именно дело
десятое. Хоть методом им. Шалыто, хоть в рамках событийной системы с очередями
сообщений.
В последнем случае есть ещё одна опасность: рекурсии и ёмкость очередей
сообщений, которые фактически уподобляются стеку. Очевидно, рекурсивный алгоритм
на очередях сообщений ведёт к их переполнению, если глубина рекурсии не ограничена
и программистом не делались её оценки. Алгоритм может быть даже не рекурсивным.
Просто положим, для всех событий используется единая очередь сообщений. И однажды
в неё попадает входное событие, в процессе обработки которого порождается несколько
синтетических сообщений, которые тоже попадают в очереь для обработки. И возникает
своеобразная лавина событий, превышающая размер очереди. Как доказать, что очереди
хватит всегда, спрашивается. Ответом на этот вопрос может служить не очередь,
а что-то вроде множества (std::set), где события имеют атомарный характер:
они происходили в прошлом или нет, но не говорится сколько раз или с какими
параметрами -- эта информация должна передаваться через отдельные и различные
механизмы, вроде fifo-очередей специфичных для событий определённого типа.
Например, переполнение буфера клавиатуры для системы в целом может быть не критично.
А если обезьяна нажимала кнопки, породила описанную выше лавину событий переполнившую
единственную глобальную очередь событий -- это катастрофа.
Возвращаясь к корутинам -- они могут быть встроенны в событийный механизм,
чтоб там вручную не выписывать switch-case для простых автоматов с несложной
логикой, где переключение между состояниями преимущественно линейное.
Здравствуйте, kov_serg, Вы писали:
_>Здравствуйте, landerhigh, Вы писали:
L>>Здравствуйте, kov_serg, Вы писали:
L>>>>Господа, а что вы вкладываете в понятие "отладка (кооперативной многозадачности|многопоточности)" и почему это такая большая проблема? Для друга интересуюсь. _>>>Очень просто потому как многозадачность в c++ не структурированая. Поэтому и отладка превращается в: поймай угря в ведре с угрями.
L>>А можно с примерами? Именно отладки, а не борьбы с гонками? _>Простой пример вы вызвали функцию, она породила поток, который породил еще несколько потоков. _>1. вы не знаете сколько потоков было порождено (да и сколько сейчас работает) _>2. в случае убийства потока, дочерние потоки не остановятся.
Библиотека параллельных задач (TPL) основана на концепции задачи, представляющей асинхронную операцию. В некотором смысле задача напоминает поток или ThreadPool рабочий элемент, но на более высоком уровне абстракции. Термин параллелизм задач относится к одной или нескольким независимым задачам, выполняемым одновременно. Задачи предоставляют два основных преимущества:
Более эффективное и более масштабируемое использование системных ресурсов.
В фоновом режиме задачи ставятся в очередь на ThreadPool, который был усовершенствован с помощью алгоритмов, определяющих и корректирующих количество потоков. Эти алгоритмы обеспечивают балансировку нагрузки для максимальной пропускной способности. Этот процесс делает задачи относительно упрощенными, и вы можете создать многие из них, чтобы включить детализированный параллелизм.
Более высокий уровень управления программой, чем это возможно с помощью потока или рабочего элемента.
Задачи и фреймворк, построенные вокруг них, предоставляют широкий набор API, поддерживающих ожидание, отмену, продолжения, надежную обработку исключений, детализированный статус, пользовательское планирование и многое другое.
По обеим причинам TPL является предпочтительным API для написания многопоточных, асинхронных и параллельных кодов в .NET.
Здравствуйте, B0FEE664, Вы писали:
BFE>Правильно ли я понимаю, что если в качестве обработчика асинхронного ответа использовать лямбду, которую положить контекст и которую через технологию Signal-Slot соединить с обработчиком, то будет тоже самое?
А... не очень понимаю, о чем ты. Могу точно сказать, что нужную функциональность получить конечно же можно. Вопрос в читаемости кода. Тут фишка, в том, что после обработки ассинхронного ответа ты можешь продолжить выполнение функции с места, где ты отправлял запрос, и выглядить снаружи это будет почти также как быдто запрос был синхронным. И часто это значительно выглядит значительно яснее, чем разбираться в множестве лямбд, обраюотчиков коллбеэков сигналов и слотов.
Здравствуйте, landerhigh, Вы писали:
L>Здравствуйте, ksandro, Вы писали:
K>>Ну, код все-таки не совсем был бы аналогичным, вместо всех этих co_ все равно нужно вызывать yield() везде, где хочешь отдать управление. Отладка кооперативной многозадачности тоже ооочень веселый и увлекательный процесс, как бы она ни была реализована, хотя в любом случае проще чем отлаживать multi-threading.
L>Господа, а что вы вкладываете в понятие "отладка (кооперативной многозадачности|многопоточности)" и почему это такая большая проблема? Для друга интересуюсь.
Многопоточность — это вытесняющая многозадачность, ассинхронность в одном потоке (коллбэки или корутины) — это кооперативная многозадачность. Корутина сама отдает управление другой корутине, потом та отдает управлеие обратно и тд.
Отладка многозадачности сложна, потому, что у нас много одновременно выполняющихся задач (да, я понимаю что я капитан очевидность). И у нас вместо одного понятного текущего состояния программы, в которое мы можем прийти некоторым конечным числом способов, появляется состояние, состоящее из текущего состояние каждой задачи, прийти в это самое состояние мы можем в общем огромным количеством способов. И отследить как мы попали в таое состояние бывает очень и очень непросто.
Но в отличии в случае многопоточности у нас вообще бесконечное число способов прийти в данное состояние, плюс любая операция записи может нафиг разрушить целостность памяти. И нет никакого способа проверить корректность.
Здравствуйте, ksandro, Вы писали:
K>Отладка многозадачности сложна, потому, что у нас много одновременно выполняющихся задач (да, я понимаю что я капитан очевидность). И у нас вместо одного понятного текущего состояния программы, в которое мы можем прийти некоторым конечным числом способов, появляется состояние, состоящее из текущего состояние каждой задачи, прийти в это самое состояние мы можем в общем огромным количеством способов. И отследить как мы попали в таое состояние бывает очень и очень непросто.
K>Но в отличии в случае многопоточности у нас вообще бесконечное число способов прийти в данное состояние, плюс любая операция записи может нафиг разрушить целостность памяти. И нет никакого способа проверить корректность.
Вот поэтому должны быть явно описаны требования, гарантии и инварианты. И желатьельно что бы их было мало и они были простые. Иначе будет бардак и "никакого способа проверить корректность".
Здравствуйте, kov_serg, Вы писали:
_>Здравствуйте, ksandro, Вы писали:
K>>Отладка многозадачности сложна, потому, что у нас много одновременно выполняющихся задач (да, я понимаю что я капитан очевидность). И у нас вместо одного понятного текущего состояния программы, в которое мы можем прийти некоторым конечным числом способов, появляется состояние, состоящее из текущего состояние каждой задачи, прийти в это самое состояние мы можем в общем огромным количеством способов. И отследить как мы попали в таое состояние бывает очень и очень непросто.
K>>Но в отличии в случае многопоточности у нас вообще бесконечное число способов прийти в данное состояние, плюс любая операция записи может нафиг разрушить целостность памяти. И нет никакого способа проверить корректность.
_>Вот поэтому должны быть явно описаны требования, гарантии и инварианты. И желатьельно что бы их было мало и они были простые. Иначе будет бардак и "никакого способа проверить корректность".
Можно написать сотню страниц всяких требований и инвариантов. Еще стоню страниц на тему как правильно все эти требования описывать. Но это никак не гарантирует тебе, что через 10 лет поддержки кто-нибудь не обратится к переменной не из того потока.
З.Ы. Был как-то реальный случай, убрали одну ненужную строчку логирования, которая давно была в коде и всем мешала. Прогнали тесты все ок, да и какие проблемы может вызвать такое мелкое изменение. Но оказалось, что в коде много лет сидел race condition, эта никому не нужная запись в лог давала задержку в несколько микро или даже нано секунд, благодаря этому ошибка не проявлялась, а в продакшене вдруг стала периодически ни с того ни с сего вылезать. И это был код, который много лет проработал в продакшене, и был давно отлажен и протестирован.
Здравствуйте, ksandro, Вы писали:
K>З.Ы. Был как-то реальный случай, убрали одну ненужную строчку логирования, которая давно была в коде и всем мешала. Прогнали тесты все ок, да и какие проблемы может вызвать такое мелкое изменение. Но оказалось, что в коде много лет сидел race condition, эта никому не нужная запись в лог давала задержку в несколько микро или даже нано секунд, благодаря этому ошибка не проявлялась, а в продакшене вдруг стала периодически ни с того ни с сего вылезать.
А я как-то (давно уже) пытался отловить race conditions при помощи отладочной печати Вот именно это и происходило — отладочная печать вносила синхронизацию и проблема переставала воспроизводиться.
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, rg45, Вы писали:
R>А я как-то (давно уже) пытался отловить race conditions при помощи отладочной печати Вот именно это и происходило — отладочная печать вносила синхронизацию и проблема переставала воспроизводиться.