对堆的访问是否序列化?

Is access to the heap serialized?

每个程序员都可以快速了解多线程的一条规则是:

如果不止一个线程可以访问一个数据结构,并且至少有一个线程可能会修改该数据结构,那么您最好序列化对该数据结构的所有访问,否则您重新进入调试痛苦的世界

通常,此序列化是通过互斥锁完成的——即,想要读取或写入数据结构的线程锁定互斥锁,做任何它需要做的事情,然后解锁互斥锁以使其再次可供其他线程使用线程。

这让我明白了一点:一个进程的内存堆是一个可以被多个线程访问的数据结构。这是否意味着对 default/non-overloaded newdelete 的每次调用都由进程全局互斥锁序列化,因此是一个潜在的序列化瓶颈,会减慢多线程程序的速度?或者现代堆实现是否以某种方式避免或减轻了该问题,如果是,他们是如何做到的?

(注意:我将此问题标记为 linux,以避免出现正确但无信息的 "it's implementation-dependent" 回复,但我也有兴趣了解 Windows 和 MacOS/X 也这样做,如果不同的实现有显着差异)

newdeletethread safe

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确实是use a lock。如果你担心你的分配造成瓶颈,写你自己的分配器。

答案是肯定的,但实际上通常这不是问题。 如果这对您来说是个问题,您可以尝试用 tcmalloc 替换您的 malloc 实现,这会减少但不会消除可能的争用(因为只有 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.

还有其他选项,例如使用自定义 allocators and/or specialized containers and/or 重新设计您的应用程序。

当你试图避免 答案是 architecture/system 依赖 试图避免多线程必须序列化访问的问题,这只发生在增长的堆上或当程序需要扩展它或return部分到系统时收缩。

第一个答案必须简单它依赖于实现,没有任何系统依赖性,因为通常情况下,库会获得大块内存作为堆的基础,并且它们在内部管理这些内存,这使得问题实际上与操作系统和体系结构无关。

第二个答案是,当然,如果所有线程只有一个堆,则可能会出现瓶颈,以防所有活动线程都争用单个内存块。有几种方法,你可以有一个堆池来允许并行,并让不同的线程使用不同的池来处理它们的请求,认为可能最大的问题是请求内存,因为当你有瓶颈。在 returning 上没有这样的问题,因为你可以更像一个垃圾收集器,在其中排队 returned 内存块并将它们排入队列以供线程调度并将这些块放在适当的位置保存堆完整性的地方。拥有多个堆甚至允许 class 通过优先级、块大小等来确定它们,因此 class 或您将要处理的问题会降低冲突的风险。这是像 *BSD 这样的操作系统内核的情况,它使用多个内存堆,在某种程度上专用于它们将要接收的使用类型(一个用于 io-disk 缓冲区,一个用于虚拟内存映射段,一个用于用于进程虚拟内存 space 管理等)

我建议您阅读 The design and implementation of the FreeBSD Operating System,它很好地解释了 BSD 系统内核中使用的方法。这已经足够普遍了,可能很大一部分其他系统都遵循这种或非常相似的方法。