尝试使用引用计数在普通C中的线程之间传递结构。我有pthreads和gcc原子可用。我可以让它工作,但我正在寻找防弹。
首先,我使用了结构本身拥有的pthread互斥锁:
struct item {
int ref;
pthread_mutex_t mutex;
};
void ref(struct item *item) {
pthread_mutex_lock(&item->mutex);
item->ref++;
pthread_mutex_unlock(&item->mutex);
}
void unref(struct item *item) {
pthread_mutex_lock(&item->mutex);
item->ref--;
pthread_mutex_unlock(&item->mutex);
if (item->ref <= 0)
free(item);
}
struct item *alloc_item(void) {
struct item *item = calloc(1, sizeof(*item));
return item;
}
但是,意识到互斥锁不应归项目所有:
static pthread_mutex_t mutex;
struct item {
int ref;
};
void ref(struct item *item) {
pthread_mutex_lock(&mutex);
item->ref++;
pthread_mutex_unlock(&mutex);
}
void unref(struct item *item) {
pthread_mutex_lock(&mutex);
item->ref--;
if (item->ref <= 0)
free(item);
pthread_mutex_unlock(&mutex);
}
struct item *alloc_item(void) {
struct item *item = calloc(1, sizeof(*item));
return item;
}
然后,进一步实现的指针按值传递,所以我现在有:
static pthread_mutex_t mutex;
struct item {
int ref;
};
void ref(struct item **item) {
pthread_mutex_lock(&mutex);
if (item != NULL) {
if (*item != NULL) {
(*item)->ref++;
}
}
pthread_mutex_unlock(&mutex);
}
void unref(struct item **item) {
pthread_mutex_lock(&mutex);
if (item != NULL) {
if (*item != NULL) {
(*item)->ref--;
if ((*item)->ref == 0) {
free((*item));
*item = NULL;
}
}
}
pthread_mutex_unlock(&mutex);
}
struct item *alloc_item(void) {
struct item *item = calloc(1, sizeof(*item));
if (item != NULL)
item->ref = 1;
return item;
}
这里有任何逻辑错误吗?谢谢!
答案 0 :(得分:1)
我不知道通用解决方案。
能够将其减少到引用计数的原子加/减,这将是很好的。事实上,大部分时间都是必需的...所以踩过互斥体或其他伤害。
但真正的问题是同时管理引用计数和指向项目的指针。
当一个帖子来到ref()
一个项目时,它是如何找到它的?如果它还不存在,那么可能它必须创建它。如果它已经存在,它必须避免一些其他线程在引用计数递增之前释放它。
所以...你的void ref(struct item** item)
的工作原理是互斥锁保护struct item**
指针...当你持有互斥锁时,没有其他线程可以改变指针 - 所以只有一个线程可以创建项目(并递增计数0-> 1),并且只有一个线程可以销毁该项目(在递减计数1-> 0之后)。
据说计算机科学中的许多问题可以通过引入新的间接层来解决,这就是这里发生的事情。问题是所有线程如何获得项目的地址 - 假设它可能(轻柔地和突然地)消失了?答:发明间接水平。
但是,现在我们假设指向该项目的指针本身不会消失。如果指向项目的指针可以保持全局进程(静态存储持续时间),则可以简单地实现这一点。如果指向该项的指针是已分配的存储持续时间对象(的一部分),那么我们必须确保以某种方式锁定该更高级别的对象 - 以便在使用该项时指向该项的指针的地址是“稳定的” 。也就是说,更高级别的对象不会在内存中移动,并且在我们使用它时不会被破坏!
因此,锁定互斥锁后的检查if (item == NULL)
是可疑的。如果互斥锁也保护指向该项的指针,则该
但是,如果该项目是较大数据结构的一部分,并且该结构已被锁定,则您可能(根本)不需要锁定来覆盖指向该项目的指针。这取决于...正如我所说,我不知道一般解决方案。
我有一些大型动态数据结构(哈希表,队列,树等),它们由许多线程共享。大多数情况下,线程会查找并保留一段时间的项目。当系统繁忙时,它非常繁忙,并且可以推迟项目的销毁,直到事情变得更安静。所以我在大型结构上使用读/写锁,对引用计数使用原子加/减,使用垃圾收集器来实际销毁项。这里的要点是,引用计数(显然简单和自包含)递增/递减的机制选择取决于如何管理项的创建和销毁,以及线程如何拥有指向一个项目(毕竟这是参考计数的重要性)。
如果您手头有128位原子操作,可以将64位地址和64位引用计数放在一起,并执行以下操作:
ref: bar = fetch_add(*foo, 1) ;
ptr = bar >> 64 ;
if (ptr == NULL)
{
if (bar & 0xF...F)
...create item etc.
else
...wait for item
} ;
unref: bar = fetch_sub(*foo, 1) ;
if ((bar & 0xF...F) == 0)
{
if (cmp_xchg(*foo, bar, (NULL << 64) | 0)))
...free(bar >> 64) ;
} ;
其中foo
是128位组合ptr / ref-count(其存在受某些外部手段保护) - 假设64位ptr和64位计数 - 和bar
是该表单的局部变量,ptr
是void *。
如果找到指针NULL触发项目创建,则从0-> 1移动计数的第一个线程知道它们是谁,以及在创建项目之前到达的任何线程,以及指针集合,也知道他们是谁,可以等待。设置指针需要cmp_xchg()
,然后创建者会发现有多少线程在等待它。
此机制将引用计数移出项目,并将其与项目的地址捆绑在一起,这看起来很整齐 - 尽管您现在需要操作该项目的地址,以及参考地址当你操作它的引用计数时,到项目。
这取代了ref
和unref
函数中的互斥锁...但不解决引用本身受保护的问题。