Pull to refresh

Comments 14

Кроме луны ещё я знаю, когда Sleep() проснётся. У менеджера задач в винде есть time slice - 16.6 ms. Так вот поток будет разбужен или в ближайший таймслайс, если до него меньше времени, чем указано, либо в следующий. Sleep(1) может до 16 мс занять.

В винде время слайса можно менять глобально функцией mm-чего-то. Хоть 1 мс поставить. Но накладные расходы винды вырастут.

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

Активное ожидание - единственно нормальный способ точной задержки. Крутить цикл с атомиком - нагружать систему памяти. Атомики блокируют шину памяти. Лучше делать цикл на регистре в 10000 и поверх ещё цикл с атомиком

А std::this_thread::sleep в интервале до 1мкс на 1-6мс на винде, на больших значениях уже 15мс. Есть еще CreateWaitableTimerEx(CREATE_WAITABLE_TIMER_HIGH_RESOLUTION) с минимум 10мкс засыпанием, более 0.5мс уже стабильный результат.

> Атомики блокируют шину памяти.
С relaxed не должно такого быть.

На консолях ещё правда, причём там даже есть бест практис как этим рулить и настраивать. На винде, где есть системно-критичный софт, вроде антивирусов, который может вклиниваться в работу потоков к сожалению только луна, если вдруг антивирус решил дёрнуть у вас кусочки дампов памяти для анализа и стопнул воркер, там не то что в следующий таймслот не попадёшь, хорошо если через десятый отпустят. Но то конечно не совсем общий пример, у 90% игроков стоит винда, что там стоит и запущено параллельно с игрой непонятно. Помню была категория системно-пофигистских игроков, которые любили играть в тунду на 8гигах с отключенным свопом, запущенным браузером, обской и дискордом(эта инфа пишется вся в краш дамп) , естественно игра жила небыстро и недолго. Так что таймслот тут бывает не при чем даже.

Статья - супер! Спасибо!
Хоть я из параллельной вселенной (HPC), но проблемы те же. Перф, перф и еще раз перф. Если поток висит на локе, значит что то ты недодумал. Правда у нас еще и многопроцессные системы, т.е. у каждого процесса, который общается по сетке еще и пачка пачка потоков. Но стоит сказать что редко кто использует native threads. Обычно MPI+OpenMP это индустриальный стандарт.

А насчет sleep() есть у меня контрпример. В одном приложении логика работы сетевого безобразия была примерно такая:
1. Пишем незадумываясь асинхронно сообщения в буфер;
2. Как только текущая работа закончена - начинаем активно ждать, протолкнув буфер в карту и долбая ее на тему выполненных реквестов.
3. Обрабатываем следущую пачку работы исходя их того что пришло
4. На барьере опустошаем буфер и ждем остальных.

Так вот. Когда процессов было мало 8 или 16 на сервер все ненапряжно летало. Но когда машины стали большими ядер так 64 на узелок, ждать из пункта 2 превратилось в ДАЙ-ДАЙ-ДАЙ. Миллионы запросов в секунду начали класть сетевой стек, который просто не успевал всем отвечать "ничего пока нету". В этом случае помог nanosleep() с временем, равным латентности сетки.
К тому же, следует заметить, что пока это вот спит то соседний тред, который еще меланхолично что-то дожевывает, может немного ускорится ибо турба сейчас довольно чуткая и павер бюджет отдаст соседу. А обычный спинлок такого не дает. Ну если только через mwait()/monitor() но не все в это умеют.

Но когда машины стали большими ядер так 64 на узелок, ждать из пункта 2 превратилось в ДАЙ-ДАЙ-ДАЙ. Миллионы запросов в секунду начали класть сетевой стек, который просто не успевал всем отвечать "ничего пока нету". В этом случае помог nanosleep() с временем, равным латентности сетки.

Вы генерируете данные быстрее, чем сетевой стек ОС может их прожевать?
Тогда вам прямая дорога в DPDK

Мне тут бывшие коллеги показывали прилетевший крашдамп с тачки на 128ядернорм ксеоне, каких то H100 видяхах и террабайте оперативки, походу был чейто списанный сервак. Вот что значит человек любит игры, там наверное даже кризис пойдет на максималках. К сожалению движок (тундра) не был на это расчитан и игра выдавал пару фпс, фикс в движок был ограничить число воркеров до 24, вместо cpu-num - 2.

Минутка для шутки (Что выведет этот код?)

ChatGPT 4o, 3o-mini-high, o1 и DeepSeek R1 выдали, что компилятор может оптимизировать и выкинуть цикл, но ничего напечатано не будет.

P.S. ИИ ещё не готово к таким вопросам на собеседовании. А то всё спрашивают, обойдите дерево вдоль и поперёк, скучно.

Проблема мьютексов не в том, что они "медленные" (200+ тактов – это не страшно), а в том, что при блокировке они заставляют поток переключаться. Лучшим решением будут Spinlock + backoff механизмы

В Юниксах (glibc/pthread) pthread_mutex_t реализует механизм adaptive mutex, который также сначала делает spinlock, а потом уже блокируется в ОС. В Вынь, критические секции/SRWLock тоже реализуются через адаптивный механизм.

Вот прямо чтобы std::mutex проверил блокировку, и сразу ушёл спать в ОС - такого нет.

1. Потоки не должны засыпать, когда им нечего делать

Если даже несколько потоков будет загружено на 100%, то ноуты начинают визжать от радости)
Нужно прогрессивное ожидание, сначала покрутить без падения частоты ЦП, потом более долгое ожидание со снижением чатоты (до 0.5-1мс, это ограничение таймеров), потом часть потоков можено уложить спать на 15мс, а другие остаются всегда на готове.

3. Все ожидания (Wait) должны работать через OS-события (семафоры и т. д.)

Только это не просто настроить. Если у меня появилось 2 задачи и 10 потоков спят, то разбудятся все, 2 захватят задачу, а остальные снова уснут. Переключение контекста это всего 1/1000 000 от времени Sleep(1), а написать логику работы со Sleep намного проще.

Дополню: если это игра и рисуется 3д, то все потоки используются и они не засыпают на 15мс, а когда пользователь пошел тыкать UI, то часть потоков засыпает. При переключении в 3д получаем задержку 15мс пока все потоки проснутся и увидят очередь задач и все они для стриминга и фоновых расчетов, так что не критично.

А вот для сервера задержка в 15мс на выход на полную мощность может быть существенна, но там и железо расчитано на постоянную 100% нагрузку, поэтому можно спинлок крутить и тратить электричество, лишь бы мгновенно обработать таску.

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

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

проще греть камень.

Помню как я играл в новый Doom на ноуте - 4 вентилятора снизу и еще один на клавиатуру, а то пальцам горячо. И ноут начал разваливаться на 3й год.

У вас тачанка УБ в примерах с player_actions. Что с мьютексом, что тем более с атомиками. Ну если с мьютексом там скорее всего просто опечатка и пример легко починить, то вариант с атомиками в принципе неверен. Несмотря на то что "все изменения" в данных будут видны их нельзя менять/читать одновременно из разных потоков. Может вы спутали пример с двойным буффером когда происходит переключения через атомик поинтер между тем в который пишем и тем из которого читаем, тогда бы это имело смысл, но строго для 2 потоков.

Compare and swap - огонь. Делал свою молекулярную динамику на CUDA с возможностью динамического образования/разрыва ковалентных связей. Там были всякие хитрости, что атом мог поменять свой тип в ходе другого взаимодействия. С помощью вот таких атомиков удалось все сделать.

Sign up to leave a comment.

Articles