在逐出旧条目时引用线程安全缓存的设计选项

时间:2010-01-25 20:30:00

标签: c++ multithreading caching reference

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

  1. 条目具有唯一键
  2. 当缓存中的条目数超过某个限制时,旧项目将被逐出(以防止缓存过大)。
  3. 每个条目的数据都是不可变的,直到条目从缓存中删除。
  4. “读者”可以访问缓存中的条目,该条目必须在读者的生命周期内有效。
  5. 每个读者都可以在自己的线程上,所有读者都可以访问同一个缓存实例。
  6. 使用此缓存的线程安全很重要,因为我们不希望读者持有对某个条目的引用,只是让它被另一个其他线程驱逐。

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

    由于数据是不可变的,如果同一个消息的每个读者只能持有引用而不是副本,但是以某种线程安全的方式(因此不会被驱逐),这将是很好的。

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

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

6 个答案:

答案 0 :(得分:2)

我认为你有效地希望每个条目都有一个读/写锁。读者在使用时读取锁定和解锁。驱逐线程必须获得写锁定(强制所有读者在获取之前完成)。读者必须有某种方式告诉(在获取读锁之前)相关条目是否已被同时驱逐。

在缺点方面,对于大缓存(就内存而言)而言,每个条目一次锁定是昂贵的。您可以通过对一组条目使用锁定来解决这个问题 - 这会影响内存与并发。在这种情况下需要注意死锁情况。

答案 1 :(得分:2)

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

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

如果您的结构是标准容器(尚不具有线程安全性),则还必须保护容器免受不受支持的并发访问。这种保护可以很好地避免上面描述的参考计数竞争条件 - 如果你使用读写器锁保护结构,结合对象内部参考计数器的原子增量同时仍然保持读卡器锁,你将是在获得引用计数之前,任何人都不要将对象从你下面删除,因为这些mutator必须是“writers”。

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

如果您无法访问(非标准)原子增量例程,则可以使用互斥锁进行原子递增/递减,但这可能会在时间和每个对象空间中显着增加成本。 / p>

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

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

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

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

答案 2 :(得分:0)

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

答案 3 :(得分:0)

我想如果你想分享一个参考,你需要保持计数。只要你使用互锁的inc / dec函数,即使对于多个线程,这应该很简单。

答案 4 :(得分:0)

在我看来,引用计数解决方案只是棘手的,因为更新/测试所述引用计数器的驱逐必须在由互斥锁保护的临界区内。只要多个进程一次不访问引用计数器,它就应该是线程安全的。

答案 5 :(得分:0)

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