Одно правило, которое быстро усваивает каждый программист, касающееся многопоточности, звучит так:
Если более чем один поток имеет доступ к структуре данных, и по крайней мере один из потоков может изменить эту структуру данных, тогда вам лучше сериализовать все обращения к этой структуре данных, иначе вас ждет мир боли отладки..
Обычно эта сериализация выполняется через мьютекс — т. е. поток, который хочет прочитать или записать структуру данных, блокирует мьютекс, делает все, что ему нужно, а затем разблокирует мьютекс, чтобы снова сделать его доступным для других потоков.
Что подводит меня к сути: куча памяти процесса — это структура данных, доступная нескольким потокам. Означает ли это, что каждый вызов стандартных/неперегруженных new и delete сериализуется глобальным мьютексом процесса и, следовательно, является потенциальным узким местом сериализации, которое может замедлить работу многопоточных программ? Или современные реализации кучи как-то избегают или смягчают эту проблему, и если да, то как они это делают?
(Примечание: я помечаю этот вопрос linux, чтобы избежать правильного, но неинформативного ответа «это зависит от реализации», но мне также было бы интересно узнать, как это делают Windows и MacOS/X, если есть существенные различия между реализациями)
@ πάνταῥεῖ Это не так уж и медленно, по крайней мере, в таких реализациях, как glibc, которые, насколько мне известно, значительно ускоряют работу за счет использования локальных пулов потоков.
@PSkocik Да, это звучит как хорошая реализация использования локальной памяти потока для управления кучей.
@JeremyFriesner Это может зависеть не только от ОС, но и от реализации компилятора. Это делает ваш вопрос немного широким.
@ πάνταῥεῖ Re, «... используйте локальную память потока для кучи в». Не забывайте, что куча в может иметь несколько уровней реализации. Было бы вполне разумно, если бы каждый поток имел свой собственный локальный кеш свободных блоков и выделял новые блоки из общей кучи или возвращал лишние блоки в разделяемую кучу по мере необходимости.
@SolomonSlow Управление памятью в куче и реальное распределение - две пары обуви.





new и delete равны потокобезопасный
The following functions are required to be thread-safe:
- The library versions of
operator newandoperator delete- User replacement versions of global
operator newandoperator deletestd::calloc,std::malloc,std::realloc,std::aligned_alloc,std::freeCalls 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, нет, обычно запросы к операционным системам выполняются очень медленно, именно для того, чтобы избежать таких узких мест. Редко случается, что два потока запрашивают больше памяти одновременно. Но бывает и так, что при одновременном запросе куска памяти оба запроса приводят к тому, что к системе приходят два разных запроса на расширение памяти.
Часто в программировании «редкий» считается хуже, чем «обычный», потому что если редкая ситуация — это та, которую вам нужно избегать, то, если она возникает только один раз за голубую луну, возникающие проблемы труднее воспроизвести и, следовательно, труднее понять. а затем исправить или обойти.
@JeremyFriesner Вам нужно будет предоставить гораздо больше контекста вокруг вашего варианта использования, чтобы сделать какие-либо существенные заявления о поведении. Для системы, которая заботится только о пропускной способности, «редкий» означает незначительный. Из общего описания в вашем сообщении мы можем указать только общее.
Ответ положительный, но на практике как правило не проблема. Если это проблема для вас, вы можете попробовать заменить свою реализацию 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. Это достаточно общий подход, и, вероятно, большой процент других систем следует этому или очень похожему подходу.
Кажется, я был совершенно неправ в этом: stackoverflow.com/questions/796099/…