如何有效地调试共享内存中的引用计数问题?

时间:2010-02-08 07:22:28

标签: algorithm multithreading debugging shared-memory reference-counting

假设您在共享内存中有一个引用计数对象。引用计数表示使用该对象的进程数,进程负责通过原子指令递增和递减计数,因此引用计数本身也在共享内存中(它可以是对象的字段,也可以是对象可以包含指向计数的指针,如果他们协助解决这个问题,我愿意接受建议。有时,进程会有一个错误,阻止它减少计数。如何让它尽可能简单地确定哪个过程不会减少计数?

我想到的一个解决方案是为每个进程提供一个UID(可能是他们的PID)。然后当进程递减时,他们将UID推送到与引用计数一起存储的链表上(我选择了一个链表,因为你可以原子地追加到CAS)。当您想要调试时,您有一个特殊的进程,它查看共享内存中仍然存在的对象的链接列表,并且列表中没有的任何应用程序的UID是尚未减少计数的那些。

此解决方案的缺点是它具有O(N)内存使用,其中N是进程数。如果使用共享内存区域的进程数很大,并且您有大量对象,则这很快就会变得非常昂贵。我怀疑可能存在一个中途解决方案,通过部分固定大小信息,您可以通过某种方式帮助调试,即使您无法确定单个过程,也可以缩小可能进程列表的范围。或者,如果您只能检测到哪个进程没有递减,而只有一个进程没有(即无法处理2个或更多进程未能减少计数的检测),这可能仍然是一个很大的帮助。

(这个问题有更多'人类'解决方案,比如确保所有应用程序使用相同的库来访问共享内存区域,但是如果共享区域被视为二进制接口而不是所有进程都将是您编写的应用程序不受您的控制。此外,即使所有应用程序使用相同的库,一个应用程序可能在库外部有一个错误,它会破坏内存,从而防止计数减少。是的我正在使用不安全的语言,如C / C ++;)

编辑:在单一过程情况下,您将拥有控制权,因此您可以使用RAII(在C ++中)。

5 个答案:

答案 0 :(得分:7)

你可以使用每个对象只使用一个额外的整数。

将整数初始化为零。当进程递增对象的引用计数时,它将其PID转换为整数:

object.tracker ^= self.pid;

当一个进程递减引用计数时,它也会这样做。

如果引用计数保持为1,那么跟踪器整数将等于增加它但不减少它的进程的PID。


这是因为XOR是可交换的((A ^ B) ^ C == A ^ (B ^ C)),所以如果一个进程使用自己的PID对跟踪器进行异常擦除次数,则与使用{{对它进行异或运算相同1}} - 这是零,这使得跟踪器值不受影响。

您也可以使用无符号值(定义为包裹而不是溢出) - 在递增使用计数时添加PID,在递减使用时减去它。

答案 1 :(得分:1)

有趣的是,共享内存共享状态不是一个强大的解决方案,我不知道如何使其健壮。

最终,如果进程退出,则操作系统会清除其所有非共享资源。顺便说一句,这是使用进程(fork())而不是线程的重大胜利。

但是,共享资源不是。其他人打开的文件句柄显然没有关闭,并且...... 共享内存。共享资源仅在共享它们的最后一个进程退出后才会关闭。

想象一下,共享内存中有一个PID列表。一个进程可以扫描此列表以查找僵尸,但是PID可以重用,或者应用程序可能已挂起而不是崩溃,或者......

我的建议是你在每个进程之间使用管道或其他消息传递原语(有时候有一个自然的主从关系,其他时候都需要与所有人交谈)。然后,当流程终止时,您可以利用操作系统关闭这些连接,这样您的同事就可以在该事件中发出信号。此外,您可以使用ping / pong超时消息来确定对等方是否已挂起。

如果在分析后,在这些消息中发送实际数据效率太低,只要将控制通道保留在操作系统清除的某种流上,就可以使用共享内存作为有效负载。< / p>

答案 2 :(得分:1)

资源所有权最有效的跟踪系统甚至不使用引用计数,更不用说引用持有者列表了。它们只有关于内存中可能存在的每种数据类型的布局的静态信息,还有每个函数的堆栈帧的形状,并且每个对象都有一个类型指示符。因此,调试工具可以扫描每个线程的堆栈,并递归地跟踪对象的引用,直到它具有内存中所有对象的映射以及它们如何相互引用。但是,具有此功能的系统当然也具有自动垃圾收集功能。他们需要编译器的帮助才能获得有关对象和堆栈帧布局的所有信息,并且在所有情况下都无法从C / C ++中可靠地获取此类信息(因为对象引用可以存储在联合中等)。另外,它们在运行时比执行计数更好。

根据您的问题,在“退化”情况下,除了堆栈上的局部变量之外,所有(或几乎所有)进程的状态都将保存在共享内存中。此时,您将在单个进程中拥有完全等效的多线程程序。或者换句话说,共享足够内存的进程开始变得无法与线程区分开来。

这意味着您无需在问题中指定“多个进程,共享内存”部分。当你试图使用引用计数时,你面临同样的问题。那些使用线程(或无限制地使用共享内存;同样的事情)的人面临另一组问题。把两者放在一起就会让你感到痛苦。

一般而言,在可能的情况下,不要在线程之间共享可变对象是个好建议。具有引用计数的对象是可变的,因为可以修改计数。换句话说,您正在(有效)线程之间共享可变对象。

我会说,如果你使用共享内存非常复杂,需要类似于GC的东西,那么你几乎得到了两个世界中最糟糕的东西:昂贵的流程创建而没有流程隔离的优势。您已经编写了(实际上)一个多线程应用程序,您可以在其中共享线程之间的可变对象。

本地套接字是用于进程间通信的非常跨平台且非常快速的API;唯一一个在所有Unices和Windows上基本相同的工作方式。因此,请考虑将其用作最小的通信渠道。

顺便说一下,您是否在持有引用的进程中一直使用智能指针?这是你唯一希望得到参考计数的一半。

答案 3 :(得分:0)

使用以下

int pids[MAX_PROCS]
int counter;

增量

do
   find i such pid[i]=0  // optimistic
while(cas[pids[i],0,mypid)==false)
my_pos = i;
atomic_inc(counter)

递减

pids[my_pos]=0
atomic_dec(counter);

所以你知道使用这个对象的所有进程

MAX_PROCS足够大并且免费搜索 如果进程数明显低于搜索MAX_PROCS,则随机放置 会非常快。

答案 4 :(得分:0)

接下来自己做事:你也可以使用像AQTime这样的工具,它有一个引用计数的memchecker。