动态无锁内存分配器

Posted

技术标签:

【中文标题】动态无锁内存分配器【英文标题】:Dynamic Lock-free memory allocators 【发布时间】:2017-09-22 02:30:35 【问题描述】:

编写满足无锁进度保证的算法或数据结构的困难之一是动态内存分配:调用mallocnew 之类的东西不能保证以可移植方式无锁。但是,mallocnew 的许多无锁实现存在,并且还有多种无锁内存分配器可用于实现无锁算法/数据结构。

但是,我仍然不明白这实际上是如何完全满足无锁进度保证的,除非您专门将数据结构或算法限制为某些预先分配的静态内存池。但是,如果您需要动态内存分配,我不明白从长远来看,任何所谓的无锁内存分配器如何才能真正实现无锁。问题是,无论您的无锁mallocnew 多么神奇,最终您可能会耗尽内存,此时您必须求助于向操作系统请求更多内存。这意味着最终您必须调用brk()mmap() 或一些这样的低级等效项才能实际访问更多内存。而且根本无法保证这些低级调用中的任何一个都以无锁方式实现。

根本没有办法解决这个问题,(除非您使用像 MS-DOS 这样不提供内存保护的古老操作系统,或者您编写自己的完全无锁操作系统 - 这两种情况不切实际或可能。)那么,任何动态内存分配器如何才能真正实现无锁?

【问题讨论】:

你是对的,但是...由于分配器只向操作系统请求大块,不需要及时释放它们,所以对sbrk()的调用总数或任何东西都可以严格限制在无关紧要的程度。当然,这不能满足硬实时要求,但我认为动态分配即使完全无锁也不能满足这样的要求。 您是担心延迟,还是担心使用en.wikipedia.org/wiki/Non-blocking_algorithm?如果是后者,我认为大多数优秀的操作系统都无法让线程在内核中休眠,同时持有会阻止其他线程使用 mmap 的锁。因此,就编写非阻塞算法而言,我认为使用mmap 在技术上不是问题,因为只有当线程可以在持有锁的同时休眠时,锁才是问题。关键部分内的几个 CPU 缓存未命中可能是您​​遇到的最糟糕的情况。 在真实系统上运行的所有分配器都必须在某个时候耗尽内存;分配静态池时用尽的无锁分配器与操作系统认为您已经受够了用尽的普通分配器在性质上没有什么不同。 @PeterCordes:但是如果您的系统中有十几个核心,并且所有核心都在忙于分配内存[在这种情况下您可能应该考虑您的算法,但不要介意],他们都在尝试要获得同一个锁,它们都必须通过单个文件通过锁。虽然它可能不会导致进程休眠,但会导致进程等待。这是同一件事。 @MatsPetersson:是的,它显然不是 wait-free,但你可以从技术上考虑它 lock-freeobstruction-free。后者只要求在任何给定时间至少有一个线程可以向前推进。在实践中,那些计算机科学术语并不是您关心的,当存在争用时,您实际上关心的是可伸缩性。因此,无锁算法(使用原子)通常是一个胜利,即使它们不是无锁的(例如无锁队列,这里对差异进行了很好的分析:***.com/a/45910129/224132)。 【参考方案1】:

正如您自己发现的那样,基本的操作系统分配器很可能不是无锁的,因为它必须处理多个进程和各种有趣的东西,因此很难不引入某种锁。

然而,在某些情况下,“无锁内存分配”并不意味着“从不锁定”,而是“在统计上锁定如此之少以至于它并不重要”。除了最严格的实时系统之外,这对任何东西都很好。如果您的锁没有高争用,那么锁或无锁并不重要 - 无锁的目的并不是锁本身的开销,而是它成为瓶颈的难易程度系统中的每个线程或进程都必须通过这个地方才能做任何有用的事情,并且这样做时,它必须在队列中等待[它也可能不是真正的队列,它可能是“谁先醒来”或其他决定谁下一个出现的机制,在当前调用者之后]。

有几个不同的选项可以解决这个问题:

如果您有一个大小有限的内存池,您可以在启动软件时立即向操作系统请求所有内存。在内存从操作系统中分块出来后,它可以用作无锁池。明显的缺点是它对可以分配多少内存有限制。然后,您要么必须停止分配(使应用程序全部失败,要么使特定操作失败)。

当然,在 Linux 或 Windows 这样的系统中,仍然不能保证内存分配在无锁场景中意味着“即时访问分配的内存”,因为系统可以并且将在没有实际的情况下分配内存物理内存支持它,并且只有在实际使用内存时,才会将物理内存页面分配给它。这可能既涉及锁,又涉及磁盘 I/O 以将其他页面分页到交换。

对于如此严格的实时系统,单个系统调用可能争用锁的时间“太多”,解决方案当然是使用专用的操作系统,一个内部有无锁分配器的操作系统操作系统(或至少一个具有可接受的已知实时行为的操作系统 - 它最多锁定 X 微秒 [X 可以小于 1.0])。实时系统通常有一个内存池和用于回收旧分配的固定大小的桶,这可以以无锁方式完成 - 桶是一个链表,因此您可以使用原子比较和交换操作从该列表中插入/删除[可能有重试,所以虽然它在技术上是无锁的,但在竞争情况下它不是零等待时间]。

另一个可行的解决方案是拥有“每个线程池”。如果您在线程之间传递数据,这可能会变得有点复杂,但是如果您接受“为重用而释放的内存可能最终在不同的线程中”(这当然会导致“所有内存现在都位于一个线程从许多其他线程中收集并释放信息,而所有其他线程的内存都用完了”)

【讨论】:

以上是关于动态无锁内存分配器的主要内容,如果未能解决你的问题,请参考以下文章

C语言中的动态内存分配的用法举例

动态内存分配与静态内存分配

2016 - 2 - 16 动态内存分配与静态内存分配

怎么查看动态分配内存空间的大小(c语言)。

动态内存分配(c语言)

连续内存分配:内存碎片与分区的动态分配