【问题标题】:Design options for references into a thread safe cache when evicting older entries驱逐旧条目时引用线程安全缓存的设计选项
【发布时间】:2010-01-26 18:47:24
【问题描述】:

我正在尝试设计一个遵循以下规则的简单缓存:

  1. 条目具有唯一键
  2. 当缓存中的条目数量超过一定限制时,旧的条目将被逐出(以防止缓存变得太大)。
  3. 在从缓存中删除该条目之前,每个条目的数据都是不可变的。
  4. “读取器”可以访问缓存中的条目,并且该条目必须在读取器的生命周期内有效。
  5. 每个读取器都可以在自己的线程上,并且所有读取器都访问同一个缓存实例。

这个缓存的线程安全很重要,因为我们不希望读者持有对条目的引用,只是让它被其他地方的另一个线程驱逐。

因此,我当前的实现只是在从缓存中读取时复制整个条目。这对于较小的对象来说很好,但是一旦对象变得太大,就会进行太多的复制。对于访问同一个缓存条目的大量读者来说,这也不是很好。

由于数据是不可变的,如果同一消息的每个读者都可以只持有引用而不是副本,但以某种线程安全的方式(这样它不会被驱逐),那就太好了。

以前的实现使用引用计数来实现这一点...但是线程非常棘手,我采用了这种更简单的方法。

我可以使用其他模式/想法来改进此设计吗?

【问题讨论】:

    标签: c++ multithreading caching reference


    【解决方案1】:

    在没有能够执行垃圾收集的更高功率的本机系统(例如 VM)中,在性能或复杂性方面不会比引用计数更好。

    你是对的,引用计数可能很棘手 - 不仅增量和减量必须是原子的,而且你需要确保在你能够增加它之前不能从你下面删除对象。因此,如果将引用计数器存储在对象中,则必须以某种方式避免在从缓存中读取指向对象的指针和设法递增指针之间发生的竞争。

    如果您的结构是标准容器,它还不是线程安全的,您还必须保护容器免受不受支持的并发访问。这种保护可以很好地与避免上述引用计数竞争条件相吻合 - 如果您使用读写器锁来保护结构,并结合对象内引用计数器的原子增量同时仍持有读卡器锁,您将在您获得引用计数之前,防止任何人从您身下删除该对象,因为此类变异器必须是“作者”。

    在这里,对象可以从缓存中逐出,同时仍然具有正引用计数 - 当最后一个未完成的引用被删除时(通过您的智能指针类),它们将被销毁。这通常被认为是一个特性,因为它意味着至少一些对象总是可以从缓存中删除,但它也有一个缺点,即内存中“活动”对象的数量没有严格的上限,因为引用计数允许对象即使在离开缓存后仍能说活着。您是否可以接受这取决于您的要求和细节,例如其他线程可以持有对对象的引用多长时间。

    如果您无权访问(非标准)原子增量例程,则可以使用互斥锁来执行原子增量/减量,尽管这可能会显着增加时间和每个对象空间的成本。

    如果您想变得更奇特(更快),您需要设计一个本身是线程安全的容器,并提出更复杂的引用计数机制。例如,您可以创建一个哈希表,其中主存储桶数组永远不会重新分配,因此可以在没有锁定的情况下访问。此外,您可以在该数组上使用不可移植的双宽 CAS(比较和交换)操作来读取指针并增加与其相邻的引用计数(64 位拱门上的 128 位内容),允许您避免上述比赛。

    完全不同的方法是实施某种“延迟安全删除”策略。这里完全避免引用计数。您从缓存中删除引用,但不要立即删除对象,因为其他线程可能仍持有指向该对象的指针。然后稍后在某个“安全”时间删除该对象。当然,诀窍在于发现何时存在这样的安全时间。基本策略涉及每个线程在“进入”和“离开”危险区域时发出信号,在此期间它们可以访问缓存并保存对包含对象的引用。一旦从缓存中删除对象时处于危险区域的所有线程都离开了危险区域,您可以释放该对象,同时确保没有更多的引用被持有。

    这有多实用取决于您的应用程序中是否具有逻辑“进入”和“离开”点(许多面向请求的应用程序都会),以及“进入”和“离开”成本是否可以在多个缓存中分摊访问。好处是没有引用计数!当然,你仍然需要一个线程安全的容器。

    通过查看链接here 的论文,您可以找到有关该主题的许多学术论文的参考以及一些实际性能考虑。

    【讨论】:

      【解决方案2】:

      我认为您实际上希望每个条目都有一个读取器/写入器锁。读者在使用它时会读取锁定和解锁。驱逐线程必须获得一个写锁(这会强制所有读者在获得之前完成)。需要有某种方法让读者知道(在获取读锁之前)相关条目是否已被同时驱逐。

      不利的一面是,对于大缓存(就内存而言)而言,每个条目一个锁是昂贵的。您可以通过对一组条目使用锁定来解决这个问题 - 这会权衡内存与并发性。在这种情况下需要小心死锁场景。

      【讨论】:

        【解决方案3】:

        听起来像一个带有 std::map 作为缓冲区的监视器在这种情况下会很有用。

        【讨论】:

          【解决方案4】:

          我在想,如果你想分享参考资料,你需要记录一下。只要您使用互锁的 inc/dec 函数,即使对于多个线程,这也应该足够简单。

          【讨论】:

            【解决方案5】:

            在我看来,引用计数解决方案的棘手之处在于更新/测试以驱逐所述引用计数器必须位于受互斥体保护的关键部分内。只要不止一个进程一次不访问引用计数器,它就应该是线程安全的。

            【讨论】:

              【解决方案6】:

              有一个循环队列,并且不允许多个线程写入,否则缓存将无用。每个线程都应该有自己的缓存,可能对其他缓存有读访问权限,但没有写访问权限。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2013-09-16
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多