Потоки против ядер, когда потоки спят

Я хочу подтвердить свои предположения о потоках и ядрах ЦП.

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

  1. Если у меня процессор с 10 ядрами и я создаю 10 потоков, каждый поток будет иметь свое собственное ядро ​​и работать одновременно.
  2. Если я запускаю 20 потоков с ЦП с 10 ядрами, то 20 потоков будут «переключаться» между 10 ядрами, предоставляя каждому потоку примерно 50% времени ЦП на ядро.
  3. Если у меня есть 20 потоков, но 10 потоков спят, а 10 активных, то 10 активных потоков будут выполняться на 100% процессорного времени на 10 ядрах.
  4. Спящий поток требует только памяти, а не процессорного времени. Пока ветка еще спит. Например, 10 000 спящих потоков используют столько же ресурсов ЦП, сколько 1 спящий поток.
  5. В общем, если у вас есть ряд потоков, которые часто спят при работе над параллельным процессом. Вы можете добавить больше потоков, чем ядер, пока не дойдете до состояния, когда все ядра заняты 100% времени.

Являются ли какие-либо из моих предположений неверными? если да то почему?

Редактировать

  • Когда я говорю, что поток спит, я имею в виду, что поток заблокирован на определенное время. В С++ я бы использовал sleep_forБлокирует выполнение текущего потока как минимум на указанное значение sleep_duration.

Что именно вы подразумеваете под "спящим"? Они находятся в "спящем (...)" вызове? Ожидание блокировки или уведомления? Ожидание ввода-вывода? Любой из вышеперечисленных?

Stephen C 12.07.2019 07:07

@StephenC В моем случае я использую en.cppreference.com/w/cpp/thread/sleep_for. Блокирует выполнение текущего потока по крайней мере на указанное значение sleep_duration.

Steven Smethurst 12.07.2019 07:11

Все предположения мне кажутся правильными.

Jeremy Friesner 12.07.2019 07:12

Планирование процессов и потоков является частью операционной системы, поэтому описанное вами поведение действительно зависит от ОС.

r3mus n0x 12.07.2019 07:36

И вы можете конкурировать за ЦП/ядра с другими пользовательскими/системными процессами... и самой операционной системой.

Stephen C 12.07.2019 08:01
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
234
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Как уже упоминалось в комментариях, это зависит от ряда факторов. Но в целом ваши предположения верны.

Спать

В старые добрые времена sleep() могла быть реализована библиотекой C как цикл, выполняющий бессмысленную работу (например, умножение 1 на 1, пока не истечет требуемое время). В этом случае ЦП все равно будет загружен на 100%. В настоящее время sleep() на самом деле приводит к тому, что обсуждение переносится на необходимое время. Такие платформы, как MS-DOS, работали таким образом, но любая многозадачная ОС имела надлежащую реализацию на протяжении десятилетий.

10 000 спящих потоков займут больше процессорного времени, потому что ОС должна принимать решения о планировании каждый тик временного интервала (каждые 60 мс или около того). Чем больше потоков нужно проверить на готовность к запуску, тем больше процессорного времени займет проверка.

Перевод резервных буферов

Добавление большего количества потоков, чем ядер, обычно рассматривается как нормальное явление. Но вы можете столкнуться с проблемой с Translate Lookaside Buffers (или их эквивалентами на других процессорах). Они являются частью управления виртуальной памятью ЦП и сами по себе являются адресной памятью содержимого. Это действительно сложно реализовать, поэтому этого никогда не бывает так много. Таким образом, чем больше выделений памяти (что будет, если вы будете добавлять все больше и больше потоков), тем больше этот ресурс съедается до такой степени, что ОС, возможно, придется начать подкачку различных загрузок TLB для того, чтобы все выделения виртуальной памяти должны быть доступны. Если это начинает происходить, все в процессе становится очень, очень медленным. Вероятно, в наши дни это менее проблематично, чем, скажем, 20 лет назад.

Кроме того, современные распределители памяти в библиотеках C (и, следовательно, все остальное, созданное поверх них, например, Java, C# и т. д.) на самом деле будут весьма осторожны в том, как обрабатываются запросы на виртуальную память, сводя к минимуму время, которое они фактически должны выполнять в качестве операционной системы. больше виртуальной памяти. По сути, они стремятся предоставить запрошенные распределения из пулов, которые у них уже есть, а не каждое malloc(), приводящее к вызову ОС. Это принимает на себя давление TLB.

Я думаю, что второй абзац вводит в заблуждение, поскольку планировщики современных ОС почти всегда спроектированы таким образом, что планировщику не нужно будет проверять все (или даже любые) спящие потоки на каждом такте временного интервала (именно потому, что, как вы указываете, выполнение так что было бы неэффективно для ЦП, когда есть много спящих потоков). Вместо этого спящие потоки полностью хранятся в отдельной структуре данных и перемещаются в эту структуру данных (или из нее) только тогда, когда происходит событие, провоцирующее перемещение.

Jeremy Friesner 13.07.2019 06:03

@JeremyFriesner, это во многом зависит от того, какие ресурсы аппаратного таймера есть у основного оборудования. В идеале ОС могла бы установить таймер для каждого спящего потока, ISR, который срабатывал, когда таймер истекал («событие»), вызывая перепланирование потока. Однако с ограниченными ресурсами таймера у ОС есть больше работы, объем которой неизбежно зависит от количества спящих потоков. Необходимо использовать несколько таймеров для создания событий, которые затем обрабатываются, чтобы определить, какие потоки следует пробуждать.

bazza 13.07.2019 08:12

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

Jeremy Friesner 13.07.2019 15:25

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

bazza 13.07.2019 22:02
Ответ принят как подходящий

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

Есть несколько факторов, которые мог заставляют поведение отклоняться от «идеального».

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

  2. Существуют различные накладные расходы, связанные с ядром операционной системы. Есть много мест, где это происходит, в том числе:

    • Управление файловой системой.
    • Управление системой физической/виртуальной памяти.
    • Работа с сетевым трафиком.
    • Планирование процессов и потоков.

    Это уменьшит ЦП, доступный для вашего приложения.

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

  4. Есть некоторые сложные взаимодействия с оборудованием, когда приложение имеет большой объем памяти, а потоки не имеют хорошей локальности памяти. По разным причинам потоки с интенсивным использованием памяти связываются друг с другом конкурировать и могут замедлять друг друга. Все эти взаимодействия учитываются как время «пользовательского процесса», но они приводят к тому, что потоки могут выполнять меньше фактической работы.


Так:

1) If I have CPU with 10 cores, and I spawn 10 threads, each thread will have its own core and run simultaneously.

Вероятно, не всегда, из-за других пользовательских процессов и накладных расходов ОС.

2) If I launch 20 threads with a CPU that has 10 cores, then the 20 threads will "task switch" between the 10 cores, giving each thread approximately 50% of the CPU time per core.

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

3) If I have 20 threads but 10 of the threads are asleep, and 10 are active, then the 10 active threads will run at 100% of the CPU time on the 10 cores.

Примерно: см. выше.

4) An thread that is asleep only costs memory, and not CPU time. While the thread is still asleep. For example 10,000 threads that are all asleep uses the same amount of CPU as 1 thread asleep.

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

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

5) In general if you have a series of threads that sleep frequently while working on a parallel process. You can add more threads then there are cores until get to a state where all the cores are busy 100% of the time.

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

Это также не учитывает планирование потоков и переключение контекста между потоками. Каждый раз, когда ОС переключает ядро ​​с одного потока на другой, она должна:

  1. Сохраните регистры старого потока.
  2. Очистите кеш памяти процессора
  3. Сделайте недействительными регистры сопоставления виртуальных машин и т. д. Сюда входят TLB, упомянутые @bazza.
  4. Загрузите регистры нового потока.
  5. Примите удары по производительности из-за необходимости выполнять больше операций чтения основной памяти и переводов страниц виртуальных машин из-за предыдущих аннулирования кеша.

Эти накладные расходы могут быть значительными. Согласно https://unix.stackexchange.com/questions/506564/ это обычно составляет около 1,2 микросекунды на переключение контекста. Это может звучать немного, но если ваше приложение быстро переключает потоки, это может составлять много миллисекунд в секунду.

Другие вопросы по теме