Сериализован ли доступ к куче?

Одно правило, которое каждый программист быстро узнает о многопоточности:

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

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

Это подводит меня к вопросу: куча памяти процесса - это структура данных, которая доступна нескольким потокам. Означает ли это, что каждый вызов по умолчанию/не перегруженный new и delete сериализуется мьютексом глобального процесса и, следовательно, является потенциальным узким местом сериализации, которое может замедлять многопоточные программы? Или современные реализации кучи избегают или смягчают эту проблему, и если да, то как они это делают?

(Примечание: я помечаю этот вопрос linux, чтобы избежать правильного, но неинформативного ответа "это зависит от реализации", но мне также было бы интересно услышать о том, как Windows и MacOS/X также делают это, если есть Существенные различия между реализациями)

Ответ 1

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

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

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

Ответ 2

new и delete потокобезопасны

Следующие функции должны быть поточно-ориентированными:

  • Библиотека версий operator new и operator delete
  • Замененные пользователем версии глобального operator new и operator delete
  • std::calloc, std::malloc, std::realloc, std::aligned_alloc, std::free

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

В gcc new реализуется путем делегирования malloc, и мы видим, что их malloc действительно использует блокировку. Если вы беспокоитесь о том, что ваше распределение вызывает узкие места, напишите свой собственный распределитель.

Ответ 3

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

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

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

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