【译文】使用原子变量和GCC在C/C++中实现引用计数

【译文】使用原子变量和GCC在C/C++中实现引用计数

译者序

原文 C/C++ reference counting with atomic variables and gcc 作者 Alexander Sandler,发布于2009年5月27日。

为了实现高性能,多线程编程要使用无锁数据结构(lock-free),引用计数是一种经常被使用的机制。但是,引用计数单打独斗是不行的,必须配合延迟删除机制(原文中的RCU方法),才可以高性能的、线程安全的访问对象。

例如下面的示例中,如果线程thread_eraser直接调用item_unref(而不是放入延迟删除队列),函数判断引用计数为0后释放内存;同一时刻线程thread_manipulator调用item_ref增加引用计数后访问对象,由于该对象已经被释放,就会出非法的内存访问(野指针)。

struct item
{
	int ref_cnt;
};
struct item* item_new(void)
{
	struct item* p=(struct item*)calloc(sizeof(struct item), 1);
	p->ref_cnt=1;
	return p;
}
void item_unref(struct item* p)
{
	if ((__sync_sub_and_fetch(&p->ref_cnt, 1) == 0))
	{
		free(p);
	}
}
void item_ref(struct item* p)
{
	__sync_add_and_fetch(&(p->ref_cnt), 1);
	return;
}
struct hash_table htable;
pthread_mutex_t lock_htable;
void thread_manipulator(void)
{
	struct item* p=NULL;
	//...
	//某些情况下,我们需要修改key1对应的对象
	pthread_mutex_lock(&lock_htable);
	p=hash_table_search(htable, key1);
	pthread_mutex_unlock(&lock_htable);
	
	item_ref(p);
	//some code work on p
	item_unref(p);
	...
}
void thread_eraser(void)
{
	struct item* p=NULL;
	//..
	//某些情况下,我们需要删除key1和其对象
	pthread_mutex_lock(&lock_htable);
	p=hash_table_remove(htable, key1);
	pthread_mutex_unlock(&lock_htable);	
	p->remove_time=time(NULL);
	queue_append_on_tail(queue, p);
	//...
	//处理队列中的延迟删除数据
	while(1)
	{
		p=queue_read_head(queue);
		if(time(NULL)-p->remove_time>60)
		{
			queue_remove(queue, p);
			item_unref(p);
		}
		else
		{
			break;
		}
	}
}

以下为翻译正文

简介

我们经常需要使用两个以上的线程访问数据结构(Data Structure,如哈希表)中的对象(Object,如哈希表中的Value)。为了达到最佳性能,我们必须区分两种保护机制:

  1. 保护数据结构自身,如哈希表。
  2. 保护数据结构中的对象,如哈希表中的Value。

引用计数用来解决什么问题?

为了理解这一问题,我们考虑下面的场景:

  • 线程1(操控者)需要从数据结构中找到一个特定的对象并修改。对应示例中的thread_manipulator线程,从hash table读取key1的value,然后修改。
  • 线程2(擦除者)删除数据结构中过期淘汰的对象。对应到示例中的thread_eraser线程,从hash table删除key1和其value。

在程序运行时,线程1和线程2有可能访问相同的对象,即竞争访问,这是典型的多线程编程场景。

很明显,两个线程必须在某种互斥机制下才能正常工作。我们可以用一个全局互斥锁(Mutex)保护整个数据结构。这种情况下,操控者线程在查找和修改对象时,必须全程持有锁。这意味着,操控者线程工作期间,擦除者线程不能访问数据结构。

某些情况下,全局互斥锁的设计将成为系统性能的瓶颈。解决该问题的唯一方法是将保护数据结构保护对象的两种线程安全机制区分开来。这样,操控者线程只需在查询对象时持有锁,一旦查询到就可以释放,然后擦除者线程就可以淘汰过期对象了。

但是,我们如何确保擦除者线程删除的对象,没有被操控者线程修改呢?很自然的,我们能会想用另一个互斥锁去保护对象本身。此刻,我们要问自己一个问题:需要多少个互斥锁来保护对象的内容?

如果用一个互斥锁保护所有的对象,这和上面全局互斥锁的方案有一样的性能瓶颈。如果每个对象用一个互斥锁,我们会陷入另一个略微不同的困境中。

假设我们为每个对象创建了一个互斥锁。我们怎么管理这些互斥锁呢?我们可以把互斥锁作为对象内的成员,但是,这又引起另一个问题:当擦除者线程决定删除一个对象时,操控者线程查询到该对象并准备获取它的锁,由于锁的持有者是擦除者线程,操控者线程会被阻塞。之后擦除者线程删除对象和其内部的锁,留着操控线程永远阻塞在一个不存在的对象。哎唷~

引用计数就是解决上述问题的一个方案,而且幸亏有原子变量,要不还得整个互斥锁保护这个引用计数。

用原子变量实现引用计数

首先,我们将对象的引用计数初始化为1;操控者线程访问对象时,要将引用计数加1,访问结束后减1。
当擦除者线程决定删除特定的对象时,它需要获得数据结构的全局锁,然后从数据结构中移除该对象,再将该对象的引用计数减1。对象的引用计数的初始值是1,对象未被使用时计数应该是0。

如果是0,我们可以安全的删除它。此时对象已从数据结构中移除,我们可以确认操控者线程不会再使用它。

如果引用计数大于0,我们需要等引用计数变为0后再删除。问题是怎么等?这个问题比较棘手,我通常会从两个方案中选一个:朴素方案和RCU方案。

朴素方案

我们创建一个待删除对象的列表,每当擦除者线程唤醒时,遍历整个列表,删除其中引用计数为0的对象。
如果列表中的对象特别多,遍历的开销会比较大,此时可以考虑RCU方案。

RCU方案

RCU是另一种同步机制,Linux内核使用的比较多,你可阅读Hierarchical RCU了解更多。我们的方案和RCU比较像。
方案的思路基于对象被操控者线程持有的时间是有上限的,过了这个时间,对象肯定不会被使用,擦除者线程就可以删掉它。
我们假定操控者线程最多需要用1分钟来修改对象。1分钟为了便于理解问题而特意夸大的。擦除者线程试图删除对象的过程如下:

  1. 获得得数据结构的锁,然后将要删除的对象从数据结构中移除。
  2. 在对象中存储当前时间。
  3. 将对象追加到待删除队列的尾部。追加到队列尾部的好处是可以元素按时间排序,越早删除的对象越在队列前面。

待删除队列
之后,擦除者线程定期访问队列头部的对象,检查它是否已经数据结构中移除超过1分钟,如果超过则检查它的引用计数是否为0:

  • 如引用计数不是0,更新对象中的删除时间,追加到队列尾部(通常不会发生)。
  • 如引用计数是0,从队列中移除对象,并删除(释放)对象。

在上图中,假定当前时间是15:35:12,队列头部对象的删除时间已超过了1分钟,因此擦除者线程可以删除该对象。接着检查队列中的下一个对象,它的删除时间还不足1分钟,所以要继续留在队列中。现在,RCU方案有一个有趣的性质:擦除者线程不需要再检查队列中的其它对象,因为我们总是在队列尾部追加对象,队列是有序的,擦除者线程可以确定其它的对象都不满足1分钟的删除时间。

相比于之前的朴素方案的全部遍历,RCU方案仅需检查队列头部的少数对象,可以节省大量的处理时间。

原子变量从哪里来?

从 gcc 4.1.2起,gcc内置了原子变量的方法,主流CPU架构都已经支持,请在使用前检查你代码运行的平台是否支持。以下是一系列原子变量的操作函数:

type __sync_fetch_and_add (type *ptr, type value);
type __sync_fetch_and_sub (type *ptr, type value);
type __sync_fetch_and_or (type *ptr, type value);
type __sync_fetch_and_and (type *ptr, type value);
type __sync_fetch_and_xor (type *ptr, type value);
type __sync_fetch_and_nand (type *ptr, type value);

type __sync_add_and_fetch (type *ptr, type value);
type __sync_sub_and_fetch (type *ptr, type value);
type __sync_or_and_fetch (type *ptr, type value);
type __sync_and_and_fetch (type *ptr, type value);
type __sync_xor_and_fetch (type *ptr, type value);
type __sync_nand_and_fetch (type *ptr, type value);

译者注: 可以参见拙作 没有atomic.h后如何在linux实现原子操作, 也可以使用C11标准库stdatomic.h

使用这些函数不需要包含任何头文件,如果你的架构不支持这些函数,链接时会报错。这些函数可以支持char,int,long,long long或其它无符号变体。

最后,我的另一篇文章介绍了原子变量的使用 multithreaded simple data type access and atomic variables

结论

希望这篇文章让你觉得有趣。如果有更多问题可以发邮件到 alex@alexonlinux.com

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值