LINUX.ORG.RU

ETL, помогите с выбором очереди разобраться

 , ,


1

2

https://www.etlcpp.com/home.html

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

Естественно, у каждой очереди на концах по одному отправителю и получателю, чтобы не искать приключений. Вместо «collision resolution» => «collision avoidance», ценой небольшого резервирования памяти (на «щели» в очереди). Объясните пожалуйста на примере stm32:

  • в какой момент с queue начнутся проблемы, и понадобится заморачиваться с более серьезными блокировками?
    • На M0-M4 для пересылки структур обычная очередь проканает, или там тоже out of order на запись в память лезет?
  • зачем надо было городить queue_spsc_locked, которому надо просовывать блокировщики, если queue_spsc_atomiс вроде и так работает?

PS. Достаточно чтобы работало с gcc/llvm.

★★★★★

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

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

из дока:

Helper functions must be supplied at construction that lock (disable) and unlock (enable) the relevant ISR.

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

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

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

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

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

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

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

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

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

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

надо спасти текущее состояние разрешений прерываний, прямо запретить то, по которому ты работаешь с очередью, а потом восстановить сохраненное состояние, но НЕ РАЗРЕШАТЬ прерывания напрямую. тебе необходимо восстановить состояние, что было на момент вызова хелпера.

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

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

атомарно нельзя сделать инкремент/декремент x, когда x в памяти. это будет чтение в регистр, инкремент/декремент, запись в память.

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

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

Еще раз посмотри внимательно, о чем я писал. Я сразу отбросил вариант, когда два процесса пишут в одну переменную. Всегда один только пишет, а второй только читает. Один отвечает за голову, второй за хвост. Ты можешь разруливать конфликты блокировками, а можешь просто их избегать. Меня интересует второй случай. На SPSC очередь он ложится как родной.

атомарно нельзя сделать инкремент/декремент x, когда x в памяти. это будет чтение в регистр, инкремент/декремент, запись в память.

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

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

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

как проверять, что очередь пуста? или переполнилась? класть без проверки на переполнение? а брать без проверки на пустоту?

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

Дык не храни вычисляемое значение в очереди. Считай на лету.

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

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

как проверять, что очередь пуста?

Если указатели на голову и хвост совпадает - считаем пустой. Самое большее чем рискуешь - прочитать старое значение. Ну на следующем опросе «догонишь», не страшно.

или переполнилась? класть без проверки на переполнение? а брать без проверки на пустоту?

Ты не учитываешь, что поведение многих систем детерминировано. И там можно подобрать длину очереди, которая ГАРАНТИРУЕТ отсутствие переполнений. В этом смысл метода. Таки да, переполнение проверять не надо. Более того, для большинства случаев длины = 2 хватит за глаза (если у тебя нет тяжелых вычислений).

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

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

Жесткая атомарность нужна далеко не всегда. Много где хватает eventual consistency. SPSC как раз такой случай по-моему. Естественно, не для многоядерных кейзов, речь же про эмбеды разной степени чахлости.

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

в чем вопрос-то? почему ОНИ делают такой класс? с хелперами? потому что они делают класс который себя корректно ведет на ограниченном буфере, и них иная задача, чем у вас.

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

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

не сдуру же они делают специальные очереди для прерываний.

а как у вас реализована выборка из очереди? как вы определяете что там есть данные?

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

в чем вопрос-то? почему ОНИ делают такой класс? с хелперами? потому что они делают класс который себя корректно ведет на ограниченном буфере, и них иная задача, чем у вас.

Это один из вопросов. Зачем вариант с хелперами, если вариант без них (_atomic) тоже вроде бы работает, и имеет аналогичное описание.

Второй вопрос - можно ли обойтись без явного std::atomic конкретно для SPSC на ARM. Не будет ли проблем с out of order и чем-нибудь еще, чего я могу не знать.

а как у вас реализована выборка из очереди? как вы определяете что там есть данные?

Ну я юзаю готовый код, писал же выше. Если вопрос, как бы делал сам для spsc - взял бы 2 указателя, на голову и хвост, и смотрел разницу. На переполнение бы забил, потому что если оно случается в реалтаймовом эмбеде - обычно уже пофик, задетектили его или нет :). С mpmc постарался бы не связываться, тем более с самопалом.

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

Второй вопрос - можно ли обойтись без явного std::atomic конкретно для SPSC на ARM. Не будет ли проблем с out of order и чем-нибудь еще, чего я могу не знать.

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

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

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

Мне интересно, можно ли использовать неявную атомарность, вместо формальной в std::. Неявная, это например:

  • при записи в память uint32 может гарантироваться что оно ни при каких обстоятельствах не окажется битым (запишется сначала несколько байт, а после прерывания остальные).
  • если используем флаги для маркирования апдейта больших объектов, то в реальную память они приземлятся в той же последовательности. Не получится, что сначала приехал флаг, что объект успешно поменяли, и только потом доехали данные самого объекта.

И т.п. Для простоты - применительно к stm32f0..stm32h7. Интересуют пояснения «вот здесь можно, а здесь уже не стоит».

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

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

при записи в память uint32 может гарантироваться что оно ни при каких обстоятельствах не окажется битым (запишется сначала несколько байт, а после прерывания остальные).

это итак гарантируется процом, если это 32 битная архитектура. проц не может перывать команду посредине, если только это не длинная команда, типа копирования одной области памяти в другую, народе movs у интела(или как оно там).

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

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

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

то есть вы ушли от блокировки за один ресурс, но заблокировали другой ресурс(ядро проца).

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

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

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

Хотелось бы вернуться к ETL, для какого-то практического выхлопа.

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

Есть просто прерывания, и один общий эвент луп, который поллит статусы очередей

так не надо делать.

правильно так.

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

а цикл выбирает их очереди рутин очередную, и запускает ее.

рутина - просто функция без параметров.

void f1(){

}

void f2(){

}

не надо полить никакие статусы. поскольку если в очередь рутин непуста - значит ее надо опустошить выбирая и запуская рутины. а если пуста - вызвать слип проца.

для одностековой конфигурации это единственно верный способ организовать правильную работу.

одноврменно эти ваши yield’ы отправляются в мусорку.

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

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

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

одноврменно эти ваши yield’ы отправляются в мусорку.

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

Vit ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.