Доступ к куче сериализуется?

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

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

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

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

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

Кажется, я был совершенно неправ в этом: stackoverflow.com/questions/796099/…

πάντα ῥεῖ 18.05.2019 08:10

@ πάνταῥεῖ Это не так уж и медленно, по крайней мере, в таких реализациях, как glibc, которые, насколько мне известно, значительно ускоряют работу за счет использования локальных пулов потоков.

PSkocik 18.05.2019 08:19

@PSkocik Да, это звучит как хорошая реализация использования локальной памяти потока для управления кучей.

πάντα ῥεῖ 18.05.2019 08:21

@JeremyFriesner Это может зависеть не только от ОС, но и от реализации компилятора. Это делает ваш вопрос немного широким.

πάντα ῥεῖ 18.05.2019 08:41

@ πάνταῥεῖ Re, «... используйте локальную память потока для кучи в». Не забывайте, что куча в может иметь несколько уровней реализации. Было бы вполне разумно, если бы каждый поток имел свой собственный локальный кеш свободных блоков и выделял новые блоки из общей кучи или возвращал лишние блоки в разделяемую кучу по мере необходимости.

Solomon Slow 18.05.2019 16:50

@SolomonSlow Управление памятью в куче и реальное распределение - две пары обуви.

πάντα ῥεῖ 18.05.2019 16:52
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
13
6
391
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

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

The following functions are required to be thread-safe:

  • The library versions of operator new and operator delete
  • User replacement versions of global operator new and operator delete
  • std::calloc, std::malloc, std::realloc, std::aligned_alloc, std::free

Calls to these functions that allocate or deallocate a particular unit of storage occur in a single total order, and each such deallocation call happens-before the next allocation (if any) in this order.

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

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

Mario 18.05.2019 10:42

@Mario, нет, обычно запросы к операционным системам выполняются очень медленно, именно для того, чтобы избежать таких узких мест. Редко случается, что два потока запрашивают больше памяти одновременно. Но бывает и так, что при одновременном запросе куска памяти оба запроса приводят к тому, что к системе приходят два разных запроса на расширение памяти.

Luis Colorado 19.05.2019 12:37

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

Jeremy Friesner 21.05.2019 05:16

@JeremyFriesner Вам нужно будет предоставить гораздо больше контекста вокруг вашего варианта использования, чтобы сделать какие-либо существенные заявления о поведении. Для системы, которая заботится только о пропускной способности, «редкий» означает незначительный. Из общего описания в вашем сообщении мы можем указать только общее.

Passer By 21.05.2019 09:53
Ответ принят как подходящий

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

TCMalloc assigns each thread a thread-local cache. Small allocations are satisfied from the thread-local cache. Objects are moved from central data structures into a thread-local cache as needed, and periodic garbage collections are used to migrate memory back from a thread-local cache into the central data structures.

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

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

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

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

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

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