我在C中实现了一个需要使用多个线程的引用计数系统。因此,我需要一种方法来减少整数引用计数,并测试一次原子操作结果是否为零。我可以使用C11和stdatomic.h
,但似乎没有减量和测试操作。
最好的(即最便携的)方式是什么?我可以使用AutoField
函数来实现此目的吗?
这是引用计数(伪代码)的核心:
id = models.AutoField(primary_key=True)
答案 0 :(得分:5)
你似乎误解了C11的原子。 Atomic限定类型,而不是单个操作。
如果用_Atomic
声明变量,则对它的所有操作都是原子的。因此,如果您对原子操作的默认“顺序一致性”(您应该)感到满意,那么您需要额外的_Atomic
限定条件。前缀--
运算符应该可以满足您的需求。
如果您想要处理不同类型的一致性,可以使用atomic_fetch_sub
,例如只有那时你才能获得修改前的值,而不是之后的值。因此,您应该将其与0
进行比较,而不是与1
进行比较。
答案 1 :(得分:0)
很抱歉在游行中下雨,但无论使用哪种原子增量/减量原语,这都可以不使用上述机制完成。
release
执行free
的瞬间,对象变为无效[我们必须假设另一个线程立即执行malloc
并重新利用内存]并且否可以通过任何线程进一步访问它。
在free
之后,不能为该对象调用retain
和release
。甚至不仅仅是探测ref_count
值。简单的ref_count
inc / dec [原子与否]不足以处理/防止这种情况。
(1)interthread锁必须位于对象对象之外,并且不得有任何alloc / free。
(2)必须通过某种列表遍历来访问对象。也就是说,有一个活动对象列表。
(3)对列表的访问由互斥锁控制。因此,实际的inc / dec [可能] 不需要是原子的[但可能是为了额外的安全]
(4)使用该列表可确保一旦对象被销毁,任何线程都不会尝试访问它,因为它已从活动对象列表中删除,并且线程不再是"参见"它
retain
和release
必须执行以下操作:
int
retain(List *list,Object *object)
{
int match = 0;
lock_list(list);
for (objnow in list) {
if (objnow is object) {
++objnow.ref_count;
match = 1;
break;
}
}
unlock_list(list);
return match;
}
int
release(List *list,Object *object)
{
int match = 0;
lock_list(list);
for (objnow in list) {
if (objnow is object) {
match = 1;
if (--objnow.ref_count == 0) {
unlink_from_list(list,objnow);
free(objnow);
match = -1;
break;
}
}
}
unlock_list(list);
return match;
}
列表上方的互斥/锁定方法也可以使用RCU
完成,但这有点复杂。
当然," list"这里不需要是一个简单的链表。它可以是B树或其他类型的容器。
一个概念:实际上,在考虑它时,如果一个对象不附加到某种全局/线程列表,ref_count
倾向于失去意义。或者更重要的是,为什么会在ref_count
上发生线上争用?
如果我们只是有一些"浮动"在列表上不的对象[或在本地每线程列表上],为什么多个线程会尝试向上/向下ref_count
,因为它更可能单个线程将拥有"那时的对象。
否则,重新架构系统可能是为了使其更具可预测性/稳定性。
<强>更新强>
线程可能不会碰撞引用计数,除非它已经有引用,因为访问该对象需要引用。
通过引用,在这里,我假设你的意思是线程已经完成retain
,会做一些事情,然后做release
。
因此,如果引用计数达到零,则当前没有线程正在访问该对象,也不会有任何线程将来这样做。因此,摧毁它是安全的。
销毁它可能是安全的,但是对于访问对象内的数据[非锁定]单元并且发生碰撞的多个线程没有互锁。
问题是让子线程执行free
。
考虑到我们有一个主线程,它创建了一个对象obj1
,并将其传递给两个线程tA
和tB
,它们在内部称为{{1}分别和objA
。
主线程启动objB
,引用计数为零。
考虑以下时间表:
obj1
对象引用计数现在为零,并且已释放内存区域。任何进一步的访问都无效。 tA: retain(objA)
tA: // do stuff ...
tA: release(objA)
可能无法以任何方式访问tB
的内存区域。
现在,我们[如果我们选择忽略]:
obj1
tB: retain(objB)
tB: // do stuff ...
tB: release(objB)
的发布会看到引用计数变为零并执行tB
。这是free
free
但是,obj1
甚至无法执行tB
,因为retain
的内存可能已被另一个线程重新分配:(1){{1}的主线程1}}或(2)另一个线程obj1
,它将内存用于完全不相关的目的
在(1)的情况下,obj2
&#39; tX
现在正在更改tB
而不是objB
在(2)的情况下,obj2
在obj1
的无关内存区域上涂鸦。即使是短暂的inc / dec也是灾难性的。
因此,在上面,有竞争条件,访问已释放的内存,双重释放,以及写入(例如)objB
,就像它是tX
一样。
那么,如果我们让主线程用refcount初始化为1而不是零呢?
现在,事情变得更好了。比赛条件被消除。但是,objtype_x
和objtype_a
永远不会看到引用数量低于1,因此 中的都不会执行tA
。因此,让单个线程执行tB
是一个没有实际意义的点。
主线程必须执行free
,这是安全的。但是,main无法知道状态free
是什么状态。也就是说,它是由free
,obj1
还是两者处理的?
所以,也许对象需要一个tA
掩码,与tB
和done
一起[原子地]进行“原子化”,主要会看这个,知道它什么时候可以1 << tA
或者,主线程,如果它知道只有两个线程1 << tB
和free
将访问该对象,它可以将refcount初始化为两个,并且两个线程可以只执行{{ 1}}当他们完成对象时。
如果tA
决定在完成自己的处理后,需要将对象发送到tB
,这样做效果不会很好。
只需引用引用,如果在 release
之前必须由tB
处理给定对象,则无法确保这一点。
从架构上讲,如果每个线程都有一个输入队列/列表[互斥锁定],整个系统可能会更好。主线程创建一个对象,将其排队到tC
。 tA
将其排队,确实有效,并将其排入tB
。每个帖子都可以做一个&#34; Y&#34;叉子。也就是说,tA
查看对象并决定将其发送到tA
,完全绕过tB
。最终,其中一个线程将对象排队回主线程(即用过的对象的空闲列表或将结果发布到main(例如map / reduce的形式))。
将一个对象放在一个[可重用的]免费列表上(与执行tA
相比)可以减轻一些因素,因为我们没有&#34; rug pull&#34;执行tC
[与立即tB
]的效果,因此我们可以将一些状态信息存储在对象中,即使对象是空闲&#34;。
因此,我们有一个intethread管道系统的效果。
这种方法的优点之一[我在运输生产系统中成功使用]的一个优点是,一旦一个对象排队到一个线程,该线程就会拥有&#34;对象和大多数访问都不需要是原子的。