一个c++图形数据结构的并发垃圾收集

concurrent garbage collection for a c++ graph data structure

本文关键字:并发 c++ 图形 数据结构 一个      更新时间:2023-10-16

我有一个有向图数据结构用于音频信号处理(见http://audulus.com如果你好奇)。

我希望图边是强引用,所以在没有环的情况下,std::shared_ptr可以做到这一点。唉,图中可能存在循环。

所以,我有了一个简单的并发标记-清除收集器的想法:

mutator线程将事件发送给收集器线程。收集器线程维护自己的图表示,不遍历mutator线程的图。收集器线程只是定期使用标记-清除。

事件如下(以函数调用的形式):

  • AddRoot(Node*)
  • RemoveRoot(Node*)
  • AddEdge(Node*, Node*)
  • RemoveEdge(Node*, Node*)

这个方案正确吗?收集器线程有一个较旧版本的mutator线程所看到的内容。我的直觉是,由于先前不可访问的节点在稍后的时间仍然不可访问,因此收集器线程可能会在发现不可访问的对象时立即删除它。

另外,如果它对一个mutator线程是正确的,它会对多个mutator线程工作吗?

我在这里发布了代码:https://github.com/audulus/collector。代码实际上是相当通用的。使用RootPtr<T>自动跟踪根节点。节点间的链接使用EdgePtr<T>进行管理。

收集器似乎适用于多个mutator线程(在我的应用程序和单元测试中),但我觉得需要证明正确性。

请注意(作为对@ aaronggolden下面评论的回应,从下面的评论判断,人们没有读这个): mutator线程负责以正确的顺序调用收集器函数。例如,如果mutator线程在将b赋值给RootPtr之前调用了RemoveEdge(a,b),那么收集器可能会介入并收集b

更新2 :

我已经将代码更新到我的最新版本,并更新了上面的链接。我现在已经在我的应用程序中使用了一年多的代码,并且没有将任何错误归因于它。

我认为在没有循环的情况下,该方案等价于具有原子引用计数的引用计数,这一论证在某种程度上是有说服力的(尽管我不愿意称之为证明)。

在没有循环的情况下,AddRootAddEdge对应于引用计数的递增,RemoveRootRemoveEdge对应于引用计数的递减。将事件压入队列(我使用boost::lockfree::queue)是一个原子操作,就像更新引用计数一样。

那么剩下的问题是:循环是如何在正确性方面改变图像的?简单地说,循环是图连通性的一种属性,但不会影响操作的原子性,也不会影响一个线程提前知道某件事的能力(否则会导致潜在的错误操作顺序)。

这表明,如果这个方案有一个反例,它将涉及到一些循环游戏。

这个方案正确吗?

我担心你没有安全点的概念。特别是,更新是否需要自动执行多个操作?也许这是可以的,因为你总是可以在删除之前批量添加所有的顶点和边。

另外,如果它对一个mutator线程是正确的,它会对多个mutator线程工作吗?

如果一个线程在另一个线程取根到同一子图之后丢弃根到子图,那么你必须确保你得到的消息是有序的,这意味着你不能使用逐变队列。全局队列很可能会破坏可伸缩性。

我的约束之一是GC必须是无锁的,因为我的实时DSP线程

  1. 分配器是否无锁?
  2. 如果GC不能跟上mutator(s)该怎么办?

同时,我建议考虑:

  1. 在子进程中进行分叉和收集。
  2. Dijkstra的三色标记方案的增量标记扫描。
  3. 贝克的跑步机。
  4. VCGC。
  5. 通过深度复制消息来分离每线程堆。