使用Ruby C扩展进行垃圾回收

5
我正在解决Ferret(Lucene的Ruby端口)代码中的一个Bug。Ferret代码主要是Ruby的C扩展。我在垃圾回收方面遇到了一些问题。我设法修复了它,但我不完全理解我的修复方法=)我希望有更深入了解Ruby和C扩展的人(这是我使用Ruby的第三天)可以详细说明一下。谢谢。
以下是情况:
在Ferret C代码的某个地方,我将“Token”返回给Ruby。代码看起来像:
static VALUE get_token (...)
{
  ...
  RToken *token = ALLOC(RToken);
  token->text = rb_str_new2("some text");
  return Data_Wrap_Struct(..., &frt_token_mark, &frt_token_free, token);
}

frt_token_mark调用rb_gc_mark(token->text),而frt_token_free只是使用free(token)释放了token。

在Ruby中,这段代码表示以下:

token = @input.next

基本上,@input被设置为某个对象,调用它的next方法会触发get_token C调用,返回一个令牌对象。

在Ruby领域中,我接下来做的事情类似于w = token.text.scan('\w+')

当我在while 1循环中运行此代码(为了分离我的问题)时,在某个时刻(大致上当我的Ruby进程内存占用量达到256MB时,可能是一些GC阈值),Ruby会因为出现错误而死亡,例如:

在终止的对象上调用扫描方法

或者只是核心转储。我猜想token.text已经被垃圾回收了。

我对Ruby C扩展不太了解,不知道返回的Data_Wrap_Struct对象会发生什么。在Ruby领域中,似乎赋值token =应该创建对它的引用。

我的“解决方法”/“修复方法”是在由@input引用的对象中创建一个Ruby实例变量,并将令牌文本存储在其中,以获得额外的引用。因此,C代码如下:

RToken *token = ALLOC(RToken);
token->text = rb_str_new2(tk->text);
/* added code: prevent garbage collection */
rb_ivar_set(input, id_curtoken, token->text);
return Data_Wrap_Struct(cToken, &frt_token_mark, &frt_token_free, token);

现在我已经在输入实例变量中创建了一个“curtoken”,并保存了文本的副本... 我已经小心地在@input类的free回调函数中删除/删除了这个引用。

使用此代码,它可以正常工作,因为我不再收到终止对象错误。

修复似乎对我有意义-它在curtoken中保留了对token.text字符串的额外引用,因此直到下一次调用@input.next(此时不同的token.text替换curtoken中的旧值)之前,token.text的实例才会被删除。

我的问题是:为什么以前不起作用? Data_Wrap_Structure不应该返回一个对象,在Ruby land中分配时具有有效引用并且不会被Ruby删除吗?

谢谢。


顺便说一句,我猜在C语言中,当我返回Data_Wrap_Struct时,应该真正创建一个VALUE变量,将其赋值为Data_Wrap_Struct的结果,并在某个地方保留对该VALUE变量的引用,这应该是返回VALUE的常规惯例--你需要手动保留引用。我认为这是比我之前展示的更好的修复方法。但请发表评论。谢谢。 - OverClocked
你搞定了吗?顺便说一下,你可以为“自定义”对象定义自己的Ruby GC标记方法... - rogerdpack
2个回答

3
当Ruby垃圾回收器被调用时,它有一个标记阶段和一个扫描阶段。标记阶段通过标记来标记系统中的所有对象:
1. 由Ruby堆栈帧(例如本地变量)引用的所有对象。 2. 所有全局可访问对象(例如由常量或全局变量引用)及其子对象/引用对象。 3. 堆栈上的引用引用的所有对象以及这些对象的子对象/引用对象。
还有一些其他对于此讨论不重要的对象。然后,扫描阶段销毁任何不可访问的对象(即未被标记的对象)。
Data_Wrap_Struct返回一个对象的引用。只要该引用对于Ruby代码可用(例如存储在本地变量中)或位于堆栈上(由本地C变量引用),则不应该扫描该对象。
从您发布的内容看,似乎token->text正在被垃圾回收。但是为什么会被回收呢?它一定没有被标记。Token对象本身是否被标记?如果是,则token->text应该被标记。尝试在标记函数中设置断点或打印消息来查看。
如果标记没有被标记,那么下一步就是找出原因。如果已经被标记,那么下一步就是找出为什么text()方法返回的字符串会被清除(也许不是同一个被标记的对象)。
另外,你确定是token的text成员导致了异常吗?看一下:

http://github.com/dbalmain/ferret/blob/master/ruby/ext/r_analysis.c

我发现Token和Token Stream都有text()方法。TokenStream结构体没有保存对其text对象的引用(它不能,因为它是一个C结构体,不知道Ruby)。因此,包装C结构体的Ruby对象需要保存引用(使用rb_ivar_set实现)。
RToken结构体不需要这样做,因为它在其标记函数中标记了其text成员。
还有一件事:您可以通过在循环中显式调用GC.start而不是必须分配许多对象以使垃圾收集器启动来重现此错误。这不会解决问题,但可能会使诊断更简单。

1
保罗:问题是从C过程返回一个值到Ruby环境中,应该做什么才是“正确的”?Ferret使用了: return Data_Wrap_Struct ... 在这种情况下,Data_Wrap_Struct返回的内容既不在C堆栈上,因为它是从C过程返回的,也不在Ruby对象中。我的解决方法是不要“return Data_Wrap_Struct”,而是 VALUE v = Data_Wrap_struct... rb_ivar_set (.., &v); return v; 这样就解决了。我想确认一下:不能直接从C返回Data_Wrap_Struct,但需要将其引用在Ruby对象中以防止被回收。谢谢。 - OverClocked

0

该链接已经失效,参考资料我找到了这个:https://dev59.com/dXfZa4cB1Zd3GeqPTZQd - Ciro Spaciari

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接