Сериализован ли доступ к куче? - PullRequest
13 голосов
/ 18 мая 2019

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

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

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

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

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

Ответы [ 3 ]

5 голосов
/ 18 мая 2019

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 действительно использует блокировку . Если вы беспокоитесь о том, что ваше распределение вызывает узкие места, напишите свой собственный распределитель.

2 голосов
/ 18 мая 2019

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

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

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

1 голос
/ 19 мая 2019

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

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

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

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

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