LINUX.ORG.RU

Пара тупых вопросов о системном программировании

 , , , ,


0

3

В раннем детстве, лет так 20 назад, я при помощи старого папиного ноута с Вин95 и QBasic пытался писать свои первые хэлловорлды. Там использовался брутально-прямолинейный способ сделать паузу в исполняемом потоке:

FOR I = 1 TO 100000
NEXT

Потом, когда я стал постарше, мне объяснили, что это напрасный перевод процессорного времени.

Теперь стало интересно, как дело обстоит в современных ОС. Если я напишу цикл, сидящий в отдельном треде, вроде

for (;;) {
    if (some_condition) {
        break;
    } else {
        continue;
    }
{

насколько это будет расточительно с точки зрения ресурсов? Предположим, у нас есть куча дочерних процессов, которые нерегулярно, время от времени выдают что-то в stdout/stderr, это нужно ловить в родительском процессе, парсить и как-то на это реагировать. Насколько такой прямолинейный подход будет эффективным? Какие есть альтернативы? Насколько разумно будет вообще заморачиваться по этому поводу, если речь идёт не о тысячах соединений в секунду? Как такие вещи оптимизируются ОС/рантаймом/компилятором?

★★★★★

https://en.wikipedia.org/wiki/Busy_waiting

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

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

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

dbus высокоуровневая платформо-специфическая штука. Меня интересует общий подход.

meliafaro ★★★★★
() автор топика

Чтобы подождать, в современных ОС есть sleep (3), который не грузит процессор. Грузить процессор пустым циклом не нужно.

А конкретно для твоей задачи есть select (2).

Legioner ★★★★★
()

Со времен кувасика ничего не изменилось. Надо делать sched_yield, sleep, poll или ещё что-нибудь в этом духе.

pinus_nigra
()
Ответ на: комментарий от timdorohin

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

Как-то так, видимо, и придётся делать.

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

Посмотри на boost или Qt, там вроде это всё есть в кросс-платформенном виде. В Rust вроде tokio есть, но точно не скажу, есть ли там поддержка вызова внешних программ, вроде он больше по сетевым соединениям.

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

Есть устаревший POSIX usleep() из unistd.h. Есть актуальные POSIX nanosleep() / clock_nanosleep() из time.h.

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

Да, вот только это уровень системных вызовов( Вот бы чего-то портабельного.

В крестах:

std::this_thread::sleep_for(std::chrono::milliseconds(200));
Pavval ★★★★★
()
Ответ на: комментарий от Legioner

В Qt это очень хорошо сделано, везде бы так. В GTK тоже неплохо, хотя и более низкоуровнево. tokio в себя вобрал все недостатки Qt - там тоже куча макросов, за которой идиоматику языка не видно. Ну то есть это как отдельное подмножество языка разбирать. Ну и да, это всё-таки про работу с сетью.

meliafaro ★★★★★
() автор топика

Если я напишу цикл, сидящий в отдельном треде, вроде

Теперь это выглядит как-то так:

pthread_mutex_lock(&mut);
while (x <= y) {
   pthread_cond_wait(&cond, &mut);
}
/* операции над x, y */
pthread_mutex_unlock(&mut);

Смотри man pthread_cond_wait

gag ★★★★★
()

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

расточительно на 100%

busy-wait оправдан только в ядре для зависаний на микро/наносекунды, где вызов примитивов синхронизаций займёт больше времени, чем само ожидание, у приличной программы в юзерспейсей busywait-ов быть не должно в принципе

Предположим, у нас есть куча дочерних процессов, которые нерегулярно, время от времени выдают что-то в stdout/stderr, это нужно ловить в родительском процессе, парсить и как-то на это реагировать. Насколько такой прямолинейный подход будет эффективным? Какие есть альтернативы?

select()/poll()/WaitForSingleObject() и т.д. либо использовать библиотеки-обёртки надо всем этим, вроде libevent, libuv

Как такие вещи оптимизируются ОС/рантаймом/компилятором?

Никак не оптимизируются, это вопрос правильного использования системных вызовов

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

Никак не оптимизируются, это вопрос правильного использования системных вызовов

Да норм, подумаешь, отожрет одно ядро. У него еще штук 7 найдется. А там глядишь и 16-ядерные подвезут. Можно еще майнер запихнуть какой-нибудь.

anonymous
()
Ответ на: комментарий от Harald

оправдан

уже нет. на замену этой порнографии запилили umwait, в каких-нибудь арм наверное и раньше такое было

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

интересно

но во все существующие процессоры это же не запилят, так что для переносимости всё равно придётся busywait юзать

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

select() достаточно портабельный

Всю жизнь, сколько помню, отдавались системе через selеct. Даже с пустыми дескрипторами, если только пауза нужна.

Oleg_Iu
()

Для плюсов: https://en.cppreference.com/w/cpp/thread
Для rust https://doc.rust-lang.org/std/sync/index.html

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

Для асинхронного чтения-записи смотри какой-нибудь Boost.Asio ну или tokio под rust.

WatchCat ★★★★★
()
Последнее исправление: WatchCat (всего исправлений: 1)

Потом, когда я стал постарше, мне объяснили, что это напрасный перевод процессорного времени.

Всё правильно сказали.

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

Совсем. Не делай так, никогда.

Какие есть альтернативы?

select, poll, epoll, высокоуровневые очереди/циклы событий (библиотеки и фреймворки для событийного программирования)

Насколько разумно будет вообще заморачиваться по этому поводу, если речь идёт не о тысячах соединений в секунду?

Не делать busy wait? Максимально разумно.

Как такие вещи оптимизируются ОС/рантаймом/компилятором?

Никак.

intelfx ★★★★★
()
Последнее исправление: intelfx (всего исправлений: 2)

В микроконтроллерах нормального уровня (а не идиотских аврках восьмибитных) гарантированную задержку можно сделать исключительно на таймере. А уж про процессоры вообще молчу. Простейший вариант - цикл с select, где ты проверяешь, не был ли он прерван каким-нибудь сигналом (если прерван — продолжаешь дальше). sleep в данном случае не годится, т.к. он не устойчив к воздействию сигналов. А уж тупой цикл с нопами у тебя вообще даст совершенно непредсказуемую задержку.

Eddy_Em ☆☆☆☆☆
()
Ответ на: комментарий от Eddy_Em

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

Eddy_Em ☆☆☆☆☆
()

Вот так правильно будет:

for (;;) { 
    if () { ... } else { ... }
    sleep(1);
}

sleep(1) сигнализирует планировщику ОС, что можно забрать остаток выделенного данной программе куска процессорного времени и передать ее другой программе. Можешь сделать две версии (со слип и без) и увидеть разницу в нагрузке на CPU.

Norgat ★★★★★
()
Последнее исправление: Norgat (всего исправлений: 1)

if (some_condition) {
break;

man condition variable. Есть во всех ОС.

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

[code] for (;;) { if () { … } else { … } sleep(1); [/code]

Не надо так делать. Это приводит к CPU wakeup и сокращает срок работы от батареи.

X512 ★★★★★
()

IO Completion Ports/epoll/kqueue

Никаких тупых бизивейтов, да и вообще, никаких тупых вейтов и слипов не используй. yield’ов всяких тоже.

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

lovesan ★★★
()

Так можно сделать, только если нужно очень короткое ожидание какого-нибудь события (ожидание инициализации переменных другим потоком и т.д.). Собственно этот примитив синхронизации и называется «активное ожидание».

В любых других случаях надо использовать правильные примитивы синхронизации (сделать reactor, например) и поллинг.

SkyMaverick ★★★★★
()

Топите с пеною у рта за раст, как панацею от всего, а сами таких базовых вещей не знаете. Ожидаемо.

Ответы многих регистрантов тут в точку. Event loop здорового человека это как минимум select/poll/epoll, но никаких блокирующих циклов.

Дополню, что с помощью timerfd и signalfd в этот цикл обработки можно добавить и таймеры и обработку сигналов.

anonymous
()
Ответ на: комментарий от izzholtik

s/никогда/почти никогда/

А вообще, если тебе действительно нужно отмерить отрезок времени «в десяток раз меньше системного таймера», а не сколько придётся, то в кернелспейсе у тебя есть hrtimers (enter HPET/TSC/whatever), а в юзерспейсе в общем случае эта задача не решается.

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

Оно физически есть на RPI?

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

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

Оно физически есть на RPI?

Что «оно»?

HPET и TSC — это x86/ACPI, но какие-то архитектурные таймеры в ARMv7/v8 совершенно точно есть.

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

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


Я тут почитал, начиная с какого-то момента clock_nanosleep, timer_create и компания теперь вполне работают через hrtimers. (Мне сначала казалось, что это фича -rt патчсета.) И никаких busy wait-ов, опять же, не нужно.

intelfx ★★★★★
()
Последнее исправление: intelfx (всего исправлений: 3)

Самое-самое-самое тупое, эффективное по использованию ресурсов проца, и портабельное решение — это через системный вызов отдавать время ядра другому процессу, через sched_yield (или Sleep(0) для оффтопа). Далее, если блокировка может сняться другим потоком быстрее, чем происходит переключение контекста, можно сделать оптимизацию в виде короткого спинлока с длительностью менее половины переключения контекста. На x86 начиная с четвертого пенька, поздних ARM и MIPS есть инструкции PAUSE и YIELD, которые ждут следующей операции кэша вместо того, чтобы попусту грузить ядро. Вот так это используется в хроме:

https://chromium.googlesource.com/chromium/src/third_party/WebKit/Source/wtf/...

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

Самое-самое-самое тупое, эффективное по использованию ресурсов проца, и портабельное решение — это через системный вызов отдавать время ядра другому процессу, через sched_yield

// The YIELD_THREAD macro tells the OS to relinquish our quanta. This is
// basically a worst-case fallback, and if you're hitting it with any frequency
// you really should be using proper lock rather than these spinlocks.
intelfx ★★★★★
()
Ответ на: комментарий от Legioner

Посмотри на boost или Qt, там вроде это всё есть в кросс-платформенном виде

Совет дебильный. Есть стандарт, всё есть в stl.

Qt вообще уже закопать нужно, это даже не С++.

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

Qt вообще уже закопать нужно, это даже не С++.

GUI в STL пока не завезли, так что закапывать пока рано.

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

if you're hitting it with any frequency
you really should be using proper lock rather than these spinlocks

Подпишусь под каждым словом, чо. Только «proper lock» реализовать значительно сложнее, чем этот YIELD_THREAD, особенно если речь идет про какие-то сложные условия между большим числом потоков. Конкретно этот коммент написан книжником, который держал при этом в уме простейший общий ресурс с доступом через блокировку как теоретически идеальное по эффективности решение. То, что даже мутексы в винде сначала пытаются обработать трения через спинлоки и до последнего откаладывают создание объекта синхронизации ядра, его не колышет.

byko3y ★★★★
()

Такие(обычно не совсем но похожие) циклы имеют место быть уместными в случаях, когда время ожидание сопоставимо со временем переключения контекста и/или отработки подсистем ввода вывода(не самих операций, а именно процедур пробуждения потока ото «сна»). Но это при условии, что ты ждёшь какого-то события в своей же памяти.

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

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

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

Позвольте поинтересоваться, зачем в многопоточном приложении блокирующая пауза?

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

Всю жизнь, сколько помню, отдавались системе через selеct. Даже с пустыми дескрипторами, если только пауза нужна.

Ну по всякому было. Вначале был сискол empty() а подвисаний менее секунды на пользовательском уровне не было вообще, потом появилась замена — ioctl(FIONREAD) и примерно в это же время sleep с отрицатльным аргументом как сантисекунды, а уж потом появился select(). Вот только он был разным, на разных платформах и потому тогда примерно появился заодно и в большинстве случаев именно для этого - configure, который выяснял какой собственно select() на данной платформе.

vodz ★★★★★
()
Последнее исправление: vodz (всего исправлений: 2)

тут уже накидали ответов. Но вот это требует комментария:

Теперь стало интересно, как дело обстоит в современных ОС.

«Современные ОС» могут быть самые разные. От десктопной многозадачной для суперскалярного процессора до специализированной РТ для более простых МК. Во втором случае может быть так, что и тупой цикл busy-wait будет выполняться вполне детерминированно. Так что универсальных рецептов быть не может, и по-прежнему в общем случае два основных вида ожидания (занятый цикл и прерывание) применимы для конкретных случаев.

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

tokio в себя вобрал все недостатки Qt

Тут не нужен tokio, вполне достаточно просто std::thread::sleep.

anonymous
()
Ответ на: комментарий от meliafaro

Если у тебя есть система, то в ней почти однозначно есть sleep. Если нет, то о какой портабельности может идти речь?

anonymous-angler ★☆
()
Ответ на: комментарий от Pavval

Давай поправлю

В кр::е::с::т::а::х:

Не благодари ;-)

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

Позвольте поинтересоваться, зачем в многопоточном приложении блокирующая пауза?

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

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

Портабельнее #ifdef тут ничего нет ;) либо руками, либо в обертке либ, которые в хелловорлдах не нужны

slackwarrior ★★★★★
()

Как такие вещи оптимизируются ОС/рантаймом/компилятором?

ничто такое компилятором не оптимизируется.

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

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

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

правильно - почитать что такое event-driven програмимрование(архитектура) и работать по событиям, впадая в ожидание. тогда тред снимается с планировщика и время/ресурсы проца не кушает. для этого применяются всякие очереди с ожиданием, семафоры, poll/select, conditional variables и их аналоги.

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

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.