一个c++图形数据结构的并发垃圾收集
concurrent garbage collection for a c++ graph data structure
我有一个有向图数据结构用于音频信号处理(见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 :
我已经将代码更新到我的最新版本,并更新了上面的链接。我现在已经在我的应用程序中使用了一年多的代码,并且没有将任何错误归因于它。
我认为在没有循环的情况下,该方案等价于具有原子引用计数的引用计数,这一论证在某种程度上是有说服力的(尽管我不愿意称之为证明)。
在没有循环的情况下,AddRoot
和AddEdge
对应于引用计数的递增,RemoveRoot
和RemoveEdge
对应于引用计数的递减。将事件压入队列(我使用boost::lockfree::queue)是一个原子操作,就像更新引用计数一样。
那么剩下的问题是:循环是如何在正确性方面改变图像的?简单地说,循环是图连通性的一种属性,但不会影响操作的原子性,也不会影响一个线程提前知道某件事的能力(否则会导致潜在的错误操作顺序)。
这表明,如果这个方案有一个反例,它将涉及到一些循环游戏。
这个方案正确吗?
我担心你没有安全点的概念。特别是,更新是否需要自动执行多个操作?也许这是可以的,因为你总是可以在删除之前批量添加所有的顶点和边。
另外,如果它对一个mutator线程是正确的,它会对多个mutator线程工作吗?
如果一个线程在另一个线程取根到同一子图之后丢弃根到子图,那么你必须确保你得到的消息是有序的,这意味着你不能使用逐变队列。全局队列很可能会破坏可伸缩性。
我的约束之一是GC必须是无锁的,因为我的实时DSP线程
- 分配器是否无锁?
- 如果GC不能跟上mutator(s)该怎么办?
同时,我建议考虑:
- 在子进程中进行分叉和收集。
- Dijkstra的三色标记方案的增量标记扫描。
- 贝克的跑步机。
- VCGC。
- 通过深度复制消息来分离每线程堆。
- 控制允许动态运行c++的并发操作数
- 节俭并发:未解决的外部问题
- 并发/多线程:是否可以以这种方式生成相同的输出?
- 用于在并发环境中访问 MMIO 的软件模式
- C++中的并发哈希表
- 提升 asio 并发计时器取消问题与链
- C++具有基元类型的并发队列
- 使用简单两相锁定的并发程序
- C++ 按引用或值推送的并发队列
- 允许多个互斥锁所有者或指定数量的并发代码执行
- 琐碎并发代码的吞吐量不会随着线程数量的增加而增加
- 由并发无序映射查找线程调用的函数是否安全?
- 在迭代期间并发修改映射
- 并发安全堆栈接口方法:正确与否?
- 内存模型和并发
- 重塑Microsoft的并发::d iagnostic::span,也可以检测外部跨度
- 如何在 qt 中从另一个线程运行 qt并发时关闭程序
- 并发问题:如何只有一个线程通过关键部分
- Qt并发错误:用作初始值设定项的数组
- 如何使用 c++ 实现并发文件/文本编辑?