【发布时间】:2021-03-25 04:40:15
【问题描述】:
据我了解,在多线程环境中,引用计数应该使用锁定来执行,以确保所有线程看到相同的内存快照。但是锁定会降低性能。 Vala 如何解决这个问题?
【问题讨论】:
标签: vala
据我了解,在多线程环境中,引用计数应该使用锁定来执行,以确保所有线程看到相同的内存快照。但是锁定会降低性能。 Vala 如何解决这个问题?
【问题讨论】:
标签: vala
引用计数主要在 GObject 中处理(对于GLib.Object 派生类型),而 GObject 又使用 GLib 中的 Atomic Operations。原子是一个棘手的主题。如果您想了解详细信息,最好从几年前 Herb Sutter 的Atomic Weapons 演讲开始。我建议您观看这些视频,即使您永远不会使用它们(99.9% 的程序员不应该使用它们),因为它会让您更好地了解计算机的实际工作原理。
“原子”这个名字可能有点误导;这与atomicicity 无关,尽管这是其中的一部分。从某种意义上说,操作是原子的,即更改要么全部进行,要么根本不进行,这是至关重要的,但更有趣的部分是原子作为阻止编译器重新执行的障碍。跨越障碍的排序操作。 Herb Sutter 的演讲涉及到很多细节,我不会在这里重复。
例如,考虑一个简单的不受保护的引用计数器:
typedef struct {
int reference_count = 0;
} Foo;
Foo* foo_create(void) {
Foo* foo = malloc(sizeof(Foo));
foo->reference_count = 1;
}
void ref(Foo* foo) {
++(foo->reference_count);
}
void unref(Foo* foo) {
if (--(foo->reference_count) == 0) {
free(foo);
}
}
我假设您可以看到不保护此内容的问题,因为我正在写一篇 SO 帖子而不是一本书。
我们感兴趣的具体原子操作是比较和交换(CAS),它基本上提供了安全执行该操作的能力:
bool cas(int* value, int* expected, int desired) {
if (*value == *expected) {
*value = desired;
return true;
} else {
return false;
}
}
使用它,我们将上面的引用计数实现更改为:
typedef struct {
int reference_count = 0;
} Foo;
Foo* foo_create(void) {
Foo* foo = malloc(sizeof(Foo));
/* No atomics needed, we haven't made the value public yet */
foo->reference_count = 1;
}
void ref(Foo* foo) {
int old_refcount;
int new_refcount;
do {
current_refcount = foo->reference_count;
new_refcount = current_refcount + 1;
} while (!cas (&(foo->reference_count), &old_refcount, new_refcount))
}
void unref(Foo* foo) {
int old_refcount;
int new_refcount;
do {
current_refcount = foo->reference_count;
new_refcount = current_refcount - 1;
} while (!cas (&(foo->reference_count), &old_refcount, new_refcount));
if (new_refcount == 0) {
free(foo);
} else if (new_recount < 0) {
// Double-free bug, code should not be reached!
}
}
但是锁定会降低性能。
原子也是如此。很多。但也比更高级别的锁要少得多。一方面,如果您正在使用互斥锁,那么您所做的基本上是:
对于原子,我们基本上是在乞求宽恕而不是请求许可:
然后我们只看操作是否成功(即cas()是否返回true)。
操作也小了很多,速度也快了很多;使用 mutext,您可能会获取锁,然后读取当前值,增加/减少它,然后释放锁。使用原子,CAS 操作被包裹在一条 CPU 指令中。
CPU 仍然必须通过确保下次任何其他内核(有点过于简单,因为即使在一个内核中也有多个缓存)请求读取与新数据一起呈现的数据来处理缓存一致性。换句话说,原子引用计数对性能不利,但它比互斥锁要好得多。坦率地说,如果你想要引用计数而不是跟踪垃圾收集原子,那几乎是你最不坏的选择。
【讨论】: