每个程序员都能快速了解多线程的一条规则是:
如果有多个线程可以访问一个数据结构,并且至少有一个线程可以修改该数据结构,则最好将对该数据结构的所有访问序列化,否则您将需要调试痛苦的世界。
通常,此序列化是通过互斥锁完成的-即,想要读取或写入数据结构的线程将锁定互斥锁,执行所需的任何操作,然后解锁互斥锁以使其可再次用于其他线程。
这使我明白了这一点:进程的内存堆是一个可由多个线程访问的数据结构。这是否意味着对默认/未重载的new
和delete
的每次调用都由进程全局互斥锁序列化,因此是潜在的序列化瓶颈,可以减慢多线程程序的速度?还是现代堆实现以某种方式避免或减轻了该问题,如果可以,那么它们如何做到?
(注意:我正在标记这个问题linux
,以避免出现正确但不具信息性的“依赖于实现的”响应,但是我也希望了解Windows和MacOS / X的情况如果各个实现之间存在重大差异,也可以这样做
答案 0 :(得分:5)
new
和delete
是thread safe
以下功能必须是线程安全的:
operator new
和operator delete
的库版本- 全局
operator new
和operator delete
的用户替换版本std::calloc
,std::malloc
,std::realloc
,std::aligned_alloc
,std::free
对这些分配或取消分配特定存储单元的函数的调用以单个总顺序发生,并且每个此类解除分配调用均在该顺序的下一次分配(如果有)之前发生。
在使用gcc的情况下,new
是通过委派给malloc
来实现的,我们看到他们的malloc
确实确实use a lock。如果您担心分配会导致瓶颈,请编写自己的分配器。
答案 1 :(得分:2)
答案是肯定的,但实际上通常都不是问题。 如果这对您来说是个问题,您可以尝试使用tcmalloc替换malloc的实现,这种实现会减少但不会消除可能的争用(因为只有1个堆需要在线程和进程之间共享)。>
TCMalloc为每个线程分配一个线程本地缓存。线程本地缓存满足小分配。根据需要将对象从中央数据结构移动到线程本地缓存中,并使用定期垃圾回收将内存从线程本地缓存迁移回中心数据结构中。
还有其他选项,例如使用自定义allocators和/或specialized containers和/或重新设计应用程序。
答案 2 :(得分:1)
当您通过尝试避免多个线程必须序列化访问的问题来避免答案取决于体系结构/系统时,这仅在程序需要执行以下操作时才会发生:展开或将其部分返回系统。
第一个答案必须简单地是依赖于实现,而没有任何系统依赖关系,因为通常,库会获得大量内存来建立堆,并在内部对其进行管理,这实际上导致了问题操作系统和体系结构无关。
第二个答案是,当然,如果所有线程只有一个堆,那么在所有活动线程争用单个内存的情况下,您可能会遇到瓶颈。有几种解决方法,您可以有一个堆池来允许并行性,并使不同的线程对它们的请求使用不同的池,认为最大的问题可能在于请求内存,因为拥有内存就属于这种情况。瓶颈。返回时就没有这种问题了,因为您可以像垃圾回收器那样工作,在其中将返回的内存块排队,然后将它们排队等待线程分派,并将这些块放在适当的位置以保持堆完整性。拥有多个堆甚至可以按优先级,块大小等对它们进行分类,因此通过要处理的类或问题,可以降低发生冲突的风险。例如* BSD的操作系统内核就是这样,它使用多个内存堆,某种程度上专用于它们将要接收的使用类型(一个用于io磁盘缓冲区,一个用于虚拟内存映射段,一个用于进程。虚拟内存空间管理等)
我建议您阅读The design and implementation of the FreeBSD Operating System,它很好地解释了BSD系统内核中使用的方法。这已经足够普遍了,并且可能有很大一部分其他系统采用了这种方法或非常相似的方法。