正确的迭代处理对象

昨天在写一个 AOI 模块,设计时又碰到一个对象迭代的老问题,必须谨慎对待,文以记之。

缘起:

当对象 A 进入 B 的 AOI 区域时,会触发一个 Enter 事件。这个事件处理是以回调函数的形式完成,如果回调函数中再次调用 AOI 模块,产生一次间接递归,就有可能破坏 AOI 模块内部的某些迭代过程。

更要命的是,如果回调函数内删除了一些相关对象,很有可能引起对已释放对象的访问错误。

这类问题在各种对象管理的程序框架中经常出现。除了上面提到的 AOI 模块,在 GUI 模块设计中也极其常见。下面谈谈我的解决方案吧。

首先,由于回调函数内部逻辑的不可预知性,我们一定要把实际的处理放在每个 API 实现的末尾。一旦真正的处理在中间,因为间接递归的可能性,极有可能保存在模块内部的上下文信息被其破坏。

正确的做法是在模块环境中创建一个唯一消息处理队列,把可能引发回调的消息都暂入到队列。由于队列只有 enter 和 leave 两个方法可以改变内部状态,迭代处理队列本身是不会出问题的。

另一个必须考虑的对象的释放时机。当我们用 C 或 C++ 这类没有 gc 机制的语言实现的时候,它是个相当头痛的问题。

因为,任何一个消息处理的回调函数都可能删除某些对象。而这些对象的指针极有可能放在消息队列中。不要跟我说在消息队列中放对象的智能指针,然后每个用到该对象的地方都使用智能指针访问。那样我会疯掉的,尤其是需要把回调函数这样的接口暴露给最终用户时,我可不希望在接口上暴露一个难看的智能指针类。即使拐弯没角的把接口问题解决掉,额外的性能付出也让人心里不大舒服。

解决方法有两个:

  1. 消息里不记对象的指针,而用一个进程生命期唯一 id 标识。 再用一张 hash 表做映射。对象删除后,id 不再找的到对应的对象。这个方案在上次的一篇文章 的留言中提过。

  2. 用一个间接指针。对象删除后把间接指针置一个标记。再次调用时就可以知道对象被删除了。间接指针本身占用的空间从额外的备用池里分配。定期回收,和真正的删除垃圾对象。

个人比较推荐性价比更高的第 2 方案。接下来展开谈一下细节。

所谓定期回收,应当隐藏起来让用户不可见。我们可以把回收过程放在新对象创建的时候,因为这个时候恰巧需要新的内存资源,是释放旧资源的最好时机。由于对象创建也可能发生在消息处理的过程中,我们不能在消息处理期间做这个工作。所以垃圾收集的时候必须检查消息队列为空,才可以开始。

回收分两个步骤:

一是删除垃圾对象,这个去检查间接指针中的删除标记位就可以了。

二是删除间接指针本身。当消息队列为空时,完全可以把所有间接指针整个一起拿掉。

以上问题考虑周全后,基本上万无一失了。:D