C++ 线程安全对象缓存的设计选项
Posted
技术标签:
【中文标题】C++ 线程安全对象缓存的设计选项【英文标题】:Design options for a C++ thread-safe object cache 【发布时间】:2010-01-26 13:46:41 【问题描述】:我正在为 C++ 中的数据缓存编写模板库,其中可以进行并发读取和并发写入,但不能使用相同的键。该模式可以用以下环境来解释:
-
用于缓存写入的互斥锁。
缓存中每个键的互斥锁。
这样,如果一个线程从缓存中请求一个键并且不存在,则可以为该唯一键启动锁定计算。与此同时,其他线程可以检索或计算其他键的数据,但尝试访问第一个键的线程会被锁定等待。
主要的约束是:
-
切勿同时计算键的值。
可以同时计算 2 个不同键的值。
数据检索不能锁定其他线程从其他键检索数据。
我的其他限制但已经解决的是:
-
固定(在编译时已知)最大缓存大小以及基于 MRU(最近使用)的抖动。
按引用检索(隐含互斥共享计数)
我不确定为每个键使用 1 个互斥锁是实现此目的的正确方法,但我没有找到任何其他实质上不同的方法。
您是否知道实现此功能的其他模式,或者您认为这是一个合适的解决方案?我不喜欢拥有大约 100 个互斥锁的想法。 (缓存大小约为 100 个键)
【问题讨论】:
也许第一句话说明了这一点,但我不清楚并发限制是否适用于对同一对象的读取。换句话说,两个线程可以同时使用一个现有的键吗? 是的,如果缓存中已经存在,2 个线程应该同时读取同一个键。否则,如果键不存在,则一个线程必须获得键上的锁,而第二个线程必须等待另一个线程完成计算并将对象添加到缓存中。同时允许其他线程从其他键中读取。这可以通过缓存的多读/单写互斥锁来完成;当锁定写入时,一个新条目被添加到缓存中。你可以认为是一个元组您想要锁定并且想要等待。因此,某处应该有“条件”(如类 Unix 系统上的pthread_cond_t
)。
我建议如下:
有一个全局互斥锁,仅用于添加或删除映射中的键。 映射将键映射到值,其中值是包装器。每个包装器都包含一个条件和一个可能的值。设置值时会发出条件信号。当一个线程希望从缓存中获取一个值时,它首先获取全局互斥锁。然后它在地图中查找:
-
如果该键有一个包装器,并且该包装器包含一个值,则线程有它的值并可能释放全局互斥锁。
如果该键有一个包装器但还没有值,那么这意味着某个其他线程当前正忙于计算该值。然后线程在条件下阻塞,当它完成时被另一个线程唤醒。
如果没有包装器,则线程在映射中注册一个新包装器,然后继续计算值。计算值时,它会设置值并发出条件信号。
在伪代码中是这样的:
mutex_t global_mutex hashmap_t 映射 锁(global_mutex) w = map.get(key) 如果(w == NULL) w = 新包装器 map.put(键,w) 解锁(global_mutex) v = 计算值() 锁(global_mutex) w.set(v) 信号(w.cond) 解锁(global_mutex) 返回 v 别的 v = w.get() 而(v == NULL) 解锁并等待(global_mutex,w.cond) v = w.get() 解锁(global_mutex) 返回 v在 pthreads
术语中,lock
是 pthread_mutex_lock()
,unlock
是 pthread_mutex_unlock()
,unlock-and-wait
是 pthread_cond_wait()
,signal
是 pthread_cond_signal()
。 unlock-and-wait
原子地释放互斥体并将线程标记为等待条件;当线程被唤醒时,互斥锁会自动重新获取。
这意味着每个包装器都必须包含一个条件。这体现了您的各种要求:
没有线程长时间持有互斥锁(阻塞或计算值)。 当要计算一个值时,只有一个线程执行,其他希望访问该值的线程只是等待它可用。请注意,当一个线程希望获得一个值并发现其他线程已经在忙于计算它时,线程最终会锁定全局互斥锁两次:一次是在开始时,一次是在值可用时。一个更复杂的解决方案,每个包装器使用一个互斥锁,可以避免第二次锁定,但除非争用非常高,否则我怀疑它是否值得。
关于有许多互斥锁:互斥锁很便宜。互斥体基本上是int
,它的成本只不过是用于存储它的四个左右字节的 RAM。注意 Windows 术语:在 Win32 中,我在这里所说的互斥锁被视为“互锁区域”; Win32 在调用CreateMutex()
时创建的东西完全不同,它可以从几个不同的进程中访问,而且由于涉及到内核的往返,所以成本要高得多。请注意,在 Java 中,每个对象实例都包含一个互斥体,Java 开发人员似乎并没有对这个主题过于暴躁。
【讨论】:
【参考方案2】:您可以使用互斥池,而不是为每个资源分配一个互斥锁。当请求读取时,首先检查有问题的插槽。如果它已经标记了一个互斥锁,则阻止该互斥锁。如果不是,则为该插槽分配一个互斥锁并发出信号,将互斥锁从池中取出。一旦互斥体未发出信号,清除插槽并将互斥体返回到池中。
【讨论】:
所有这些仍然必须是线程安全的。如果两个线程同时尝试将互斥锁分配给一个插槽会发生什么? :) 完全正确。我的回答实际上只是一个指向一个基本想法的围栏。与任何多线程应用程序一样,魔鬼在细节中...... 其实这不是什么大问题,我喜欢这个主意。假设我使用多读取器/单写入器互斥锁来访问缓存,当 1 个线程想要向缓存添加密钥时,请求独占访问,添加密钥,从池中为其分配互斥锁并锁定它,释放缓存互斥体并开始计算。当没有人锁定它时,我仍然无法确定如何将互斥锁“返回”到池中。任何的想法? :)【参考方案3】:一种更简单的解决方案是在整个缓存上使用单个读取器/写入器锁。鉴于您知道条目的最大数量(并且相对较小),听起来向缓存添加新键是一个“罕见”事件。一般的逻辑是:
acquire read lock
search for key
if found
use the key
else
release read lock
acquire write lock
add key
release write lock
// acquire the read lock again and use it (probably encapsulate in a method)
endif
不了解使用模式的更多信息,我不能确定这是否是一个好的解决方案。不过,它非常简单,如果主要用于读取,则在锁定方面非常便宜。
【讨论】:
我的假设(可能不正确)是需要某种独占访问才能将新条目添加到缓存中。读锁(在读/写锁上)将确保在持有写锁的同时添加新项目时的安全性。 我虽然有这样的想法,但不太适合我的环境。我有一个热身阶段,其中大部分所需的键是由各种线程选择的,以未知的方式或顺序(取决于前一个键的结果)。在密钥被重用一段时间后,另一组计算开始。我不能在计算阶段失去并行性;因此不能使用单个写入互斥锁。以上是关于C++ 线程安全对象缓存的设计选项的主要内容,如果未能解决你的问题,请参考以下文章