一个滥用引用计数的例子

465 阅读4分钟

前言

我最近在修改一个程序错误中,发现某段数据资源总是释放不了。后做了深入调查,发现该程序大量使用了引用技术。这段该释放的资源在需要释放的时,它的计数器总是大于1。因此我针对这段资源的所有get和put操作,仔细检查了引用计数的加减的过程。就从程序的字面上看,get和put确实成对出现。但从设计的角度来说, 有的put是不会被调用的,因此对应的资源就无法释放。本文中,我想就这个例子来看看这个设计的问题出现在哪里。

基本原理

引用技术是底层编程中一种资源管理常用的技术。概要的说,是将资源(包括内存,对象和磁盘空间等)的引用次数用一个计数器记录。当计数器为0时候,将其释放。为了避免竞争,可以用计数器可以用原子操作来保护。

用例分析

在上图中,有一个数据源,可以理解为一个类似Redis的基于内存的数据库。针对不同类型的应用场景,可以设计不同的抽象数据操作。这样, 数据操作和数据源是分离的,可以看作是一种贫血模型 (anaemic domain model)。当数据源实例化时,数据源的引用计数初始化为1。因为这个数据源上还有三个抽象数据操作,同时也将三个抽象数据操作实例化。当每个抽象数据操作实例化时,数据源的引用计数加1。这样数据源实例化后的引用计数就会是4。

datasource->refcount = 4

其删除过程的标准代码是:

static void datasource_destroy(void *datasource)
{
    for (i = 0; i < 3; i++) {
        if (datasource->ops[i])
           datsource_ops_put(datasource->ops[i]);
    }
    datasource_do_destroy(datasource);
}
static void datasource_put(void) {
if (--datasource->refcount > 0)
	return;
datasource_destroy(datasource);
}

但从字面上看,每个datasource 和ops都有对应的put操作,应该可以正确删除。实际上, datasource_put操作后,其ref_count会是3,永远也不会执行到datasource_destroy的部分。 分析了问题的所在,修改是不难的。我们可以先执行的抽象数据类型的put,然后再执行 datasource的put。修改后的程序如下:

static void datasource_destroy(void *datasource) {
    datasource_do_destroy(datasource);
}

static void datasource_put(void) {
	for (i = 0; i < 3; i++) {
        if (datasource->ops[i])
           datsource_ops_put(datasource->ops[i]);
    }
	if (--datasource->refcount > 0)
		return;
	datasource_destroy(datasource);
}

进一步分析

这样修改虽然可以正确执行,但是从代码来看,我们自然提出一个问题,像这种初始化增加引用计数,删除时再减少计数的操作有何必要? 从软件设计上看,软件实体A, B之间的关系就生命周期而言主要是两种:(1)A,B之间是独立的,B的生命周期不依赖A的生命周期,也就是说可以单独销毁A而不影响B,如课程和学生的关系,UML中提到的聚合(Aggregation)。(2)B依赖A。B不能单独存在,其生命周期依赖A。如鸟和翅膀的关系,UML提到的组合(Composition)。在关系(1)中,B和A和可以独立生存,当B临时用到A时,为了防止A在用时被释放了,B应该用一个应用计数把A的内存保护起来。这是引用计数的典型场景。在关系(2)中,当B用到A时, A在另外的线程或任务中也有可能释放,但一旦释放了A,B也没有了存在的意义。因此B在定义的时候就不应该是一个独立的实体,而是应该作为A的一个组合子实体而存在。每次引用B时,可以先用Get方法得到A,然后再用B。B用完后,再释放A。而对datasource的每个抽象数据操作实例化时,是不需要将应用计数加1的。 修改后的程序如下:

  1. 初始化: 数据源实例化时,引用计数初始化1。抽象类型操作实例化,引用计数不变。
    datasource->refcount = 1;
    
  2. 程序运行中:
	datasource_ops_execute(int k) {
		datasource = datasource_get();  /* ref_count + 1 */
    	ops = datasource->ops[k]
    	...
    	datasource->put(); /* ref_count - 1 */
    }
  1. 删除数据源:
static void datasource_destroy(void *datasource) {
    datasource_do_destroy(datasource);
}

static void datasource_put(void) {
	if (--datasource->refcount > 0)
		return;
	datasource_destroy(datasource);
}

总结

引用计数似简单,要是没有对软件设计和软件实体的深入理解,也常常会引入一些低质量甚至是错误的代码。工作中常常勤于思考,是我们不断进步的源泉。