【问题标题】:How does Vala handle reference counting with multithreading?Vala 如何使用多线程处理引用计数?
【发布时间】:2021-03-25 04:40:15
【问题描述】:

据我了解,在多线程环境中,引用计数应该使用锁定来执行,以确保所有线程看到相同的内存快照。但是锁定会降低性能。 Vala 如何解决这个问题?

【问题讨论】:

    标签: vala


    【解决方案1】:

    引用计数主要在 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 仍然必须通过确保下次任何其他内核(有点过于简单,因为即使在一个内核中也有多个缓存)请求读取与新数据一起呈现的数据来处理缓存一致性。换句话说,原子引用计数对性能不利,但它比互斥锁要好得多。坦率地说,如果你想要引用计数而不是跟踪垃圾收集原子,那几乎是你最不坏的选择。

    【讨论】:

    • 感谢您深入了解细节!
    猜你喜欢
    • 1970-01-01
    • 2015-05-29
    • 2019-02-27
    • 1970-01-01
    • 2019-11-27
    • 2017-01-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多