想象一下这样的结构:
struct my_struct {
uint32_t refs
...
}
通过查找表获取指针:
struct my_struct** table;
my_struct* my_struct_lookup(const char* name)
{
my_struct* s = table[hash(name)];
/* EDIT: Race condition here. */
atomic_inc(&s->refs);
return s;
}
多线程模型中的取消引用和原子增量之间存在竞争。鉴于这是非常性能关键的代码,我想知道如何通过解除引用和原子增量来解决或解决这种竞争?
编辑:当通过查找表获取指向my_struct
结构的指针时,必须首先取消引用结构以增加其引用计数。当其他线程可能正在改变引用计数并可能释放对象本身而另一个线程将取消引用指向不存在的内存的指针时,这会在多线程代码中产生问题。加上先发制人和一些厄运,这可能是一场灾难。
答案 0 :(得分:1)
一种解决方案是使用freelist,而不是malloc()和free()。这有明显的缺点。
另一种方法是实现无锁垃圾收集(也称为安全内存回收)。
该领域有多项专利,但基于时代的LFGC似乎没有受到阻碍。
使用此方法的结果是,当没有线程指向它们时,元素才会被释放。
前一种解决方案非常容易实现。当然,您需要一个无锁的空闲列表,否则您的整个系统将不再是无锁的。
后者实际上并不复杂,但需要学习有问题的算法,这需要一些时间和研究。
答案 1 :(得分:1)
如上所述,您可以在以后的某个时间内将链接的内存列表释放,因此您的指针永远不会无效。在某些情况下,这是一种方便的方法。
或....你可以使用32位指针创建一个64位结构,并为引用计数和其他标志提供32位。如果将它包装在union中,则可以在结构上使用64位原子操作:
union my_struct_ref {
struct {
unsigned int cUse : 16,
fDeleted : 1; // etc
struct my_struct *s;
} Data;
unsigned long n64;
}
人类可以使用结构的数据部分进行读取,并且可以在n64位部分使用CAS。
my_struct* my_struct_lookup(const char* name)
{
struct my_struct_ref Old, New;
int iHash = hash(name);
// concurrency loop
while (1) {
Old.n64 = table[iHash].n64;
if (Old.Data.fDeleted)
return NULL;
New.n64 = Old.n64;
New.Data.cRef++;
if (CAS(&table[iHash].n64, Old.n64, New.n64)) // CAS = atomic compare and swap
return New.Data.s; // success
// we get here if some other thread changed the count or deleted our pointer
// in between when we got a copy of it int old. Just loop to try again.
}
}
如果使用64位指针,则需要进行128位CAS。
答案 2 :(得分:0)
除了您确定的种族之外,您还存在记忆一致性的一般问题。
即使您可以以无锁方式使表修改成为原子,但是当从不同的线程看到内存my_struct*
指向的内存块仍然是“陈旧的”与上次修改它的线程相比。这不适用于my_struct.refs
(假设您始终使用atomics访问它),但适用于所有其他字段。这是对每个CPU核心“私有”的写缓冲区和缓存的结果。
保证您看到正确内存内容的唯一方法是使用内存屏障。然而,一个典型的锁是也一个内存屏障,所以为什么不首先使用锁?
无锁编程比最初看起来要复杂得多,OTOH锁可以非常快,特别是当争用很少时。您是否真的对基于锁的实现进行了基准测试并确认锁定确实是您的瓶颈?