skynet 中使用了一个 hash 表结构来保存服务和 32bit 数字地址的映射关系。
一个 skynet 的服务,其实是一个 C 对象。在有沙盒的系统中,尤其是并行构架,我们很少直接用 C 对象指针来标识一个 C 对象,而是采用数字 id 。用数字做 handle 可以让系统更健壮,更容易校验一个对象是否还有效,还可以减少悬空指针,多次释放对象等潜在问题。比如,操作系统为了把用户程序隔离在用户态,像文件对象就是用数字 id 的形式交给用户程序去用的。
和操作系统通常管理文件句柄的方式不同,skynet 尽量不复用曾经用过的 id 。这样,当你持有了一个已经不存在的 id ,不太会误当作某个新的对象。(操作系统的文件 handle 很容易造成这样的问题:你记住了一个文件,在别的流程中关闭了它,然后又打开了新文件,结果复用了过去的 handle ,导致你操作了错误的文件。 很多年前,我就在自己的线上产品中碰到过这样的 bug 。)
但是,一旦尽量不复用 id 。用简单的数组来做映射就很难了。必须使用 hash 表来保证映射效率。在 skynet 中我是这样做的:
每次分配新的 id 时,首先递增上次分配出去的 id (允许回绕,但会跳过 0 。0 被当成无效 id )。然后检查 hash 表中对应的 slot 是否有冲突,若有冲突就继续递增。如果 hash 表的池不够用了,则成倍扩大池,并将内部的数据重新 hash 一次。
这样虽然会浪费一些 id ,但是可以保证 hash 表类的 key 永远不发生碰撞,这样可以让查询速度最快。hash 表的实现也相对简单一些。
我觉得这样的一个数据结构有一定的通用性,今天花了一点时间把 skynet 的这个部分单独抽出来,当成一个独立开源项目重新写了一遍。有兴趣的同学可以 在 github 上查看 。
这里的 struct handlemap 就是这样的一张 hash 表。而 handleid 目前被定义为 unsigned int ,可以在实际使用时定义为其它字长的整数。
一共提供了 5 个 API :
struct handlemap * handlemap_init(); void handlemap_exit(struct handlemap *);
分别是创建和销毁 handlemap 结构,没什么好说的。和 skynet 的实现不同,这里做了一些简化。在销毁 handlemap 时,暂时没有手段去清理里面那些还没有删除的 id 。
我认为在实际使用时,可以通过外部手段去解决。
handleid handlemap_new(struct handlemap *, void *ud);
就是为 ud 分配并绑定一个 id 。这个操作一般会成功,但在很少情况下会因为内存不足而失败。失败则返回 0 。 0 在整个系统中永远表示无效 id 。
void * handlemap_grab(struct handlemap *, handleid id);
取出 id 对应的对象指针,并在内部增加对它的引用。如果 id 无效,那么会返回 NULL 指针。在实现时,采用了引用计数用来满足线程安全。
void * handlemap_release(struct handlemap *, handleid id);
当使用完对象指针后,应该调用这个 api 归还 id 的引用。另外,如果希望删除这个 id 也可以调用它。无论是归还引用还是删除,都必须检查这个 api 的返回值。当返回值不为 NULL 时,表示返回的这个 ud 已经脱离了 handlemap 的管理,通常你需要删除这个对象。
这个数据结构的实现难点在于需要线程安全。在不同的线程中可以并发去 grab 对象,同时也可以安全的删除它们。
我使用了一个读写锁来保证线程安全:
在创建新 id ,或当一个 id release 的时候引用减到零,都会加上写锁,完成成 handlemap 的修改。
而获取(grab)或释放 (release )id 时,则是加的读锁,这些操作是可以并发的。
btw, 和 skynet 不同,这份实现里我支持了 windows 的原子操作 api 。但没有仔细测试,如果有兴趣的同学可以试着用 VC 编译。