繁体   English   中英

C ++线程安全对象缓存的设计选项

[英]Design options for a C++ thread-safe object cache

我正在为C ++中的数据缓存编写模板库,其中可以进行并发读取,也可以进行并发写入,但不能用于相同的密钥。 可以使用以下环境解释该模式:

  1. 缓存写入的互斥锁。
  2. 缓存中每个键的互斥锁。

这样,如果线程从缓存请求密钥并且不存在,则可以为该唯一密钥启动锁定计算。 与此同时,其他线程可以检索或计算其他键的数据,但尝试访问第一个键的线程会被锁定等待。

主要限制因素是:

  1. 切勿同时计算密钥的值。
  2. 计算2个不同键的值可以同时进行。
  3. 数据检索不得锁定其他线程从其他键检索数据。

我已解决的其他限制是:

  1. 固定(在编译时已知)最大高速缓存大小与基于MRU(最近使用)的颠簸。
  2. 通过引用检索(暗示互斥共享计数)

我不确定每个密钥使用1个互斥是实现这个的正确方法,但我没有找到任何其他实质上不同的方式。

您是否了解其他模式以实现此目的,或者您认为这是一个合适的解决方案吗? 我不喜欢有大约100个互斥锁的想法。 (缓存大小约为100键)

您可以使用互斥池而不是为每个资源分配一个互斥锁。 在请求读取时,首先检查有问题的插槽。 如果它已经标记了互斥锁,则阻止该互斥锁。 如果没有,请将互斥锁分配给该插槽并发出信号,将互斥锁从池中取出。 一旦互斥锁未通知,请清除插槽并将互斥锁返回池中。

你想锁定,你想等待。 因此,某些地方应该有“条件”(在类Unix系统上为pthread_cond_t )。

我建议如下:

  • 有一个全局互斥锁,仅用于在地图中添加或删除键。
  • 映射将键映射到值,其中值是包装器。 每个包装器都包含一个条件和一个值。 设置值时会发出条件信号。

当线程希望从缓存中获取值时,它首先获取全局互斥锁。 然后它在地图中查找:

  1. 如果该键有一个包装器,并且该包装器包含一个值,则该线程有其值并可以释放全局互斥锁。
  2. 如果存在该键的包装但没有值,那么这意味着其他一些线程当前正在忙于计算该值。 然后线程在条件上阻塞,在完成时由另一个线程唤醒。
  3. 如果没有包装器,则线程在映射中注册新的包装器,然后继续计算该值。 计算该值时,它会设置该值并发出条件信号。

在伪代码中,这看起来像这样:

mutex_t global_mutex
hashmap_t map

lock(global_mutex)
w = map.get(key)
if (w == NULL) {
    w = new Wrapper
    map.put(key, w)
    unlock(global_mutex)
    v = compute_value()
    lock(global_mutex)
    w.set(v)
    signal(w.cond)
    unlock(global_mutex)
    return v
} else {
    v = w.get()
    while (v == NULL) {
        unlock-and-wait(global_mutex, w.cond)
        v = w.get()
    }
    unlock(global_mutex)
    return v
}

pthreads术语中, lockpthread_mutex_lock()unlockpthread_mutex_unlock()unlock-and-waitpthread_cond_wait()signalpthread_cond_signal() unlock-and-wait以原子方式释放互斥锁并将线程标记为等待条件; 线程被唤醒时,会自动重新获取互斥锁。

这意味着每个包装器必须包含一个条件。 这体现了您的各种要求:

  • 没有线程长时间持有互斥锁(阻塞或计算值)。
  • 当要计算一个值时,只有一个线程执行它,希望访问该值的其他线程只是等待它可用。

请注意,当一个线程希望得到一个值并发现某个其他线程已经忙于计算它时,线程最终会锁定全局互斥锁两次:一次在开头,一次在值可用时。 一个更复杂的解决方案,每个包装器有一个互斥锁,可以避免第二次锁定,但除非争用非常高,否则我怀疑这是值得的。

关于有许多互斥体:互斥体很便宜。 互斥体基本上是一个int ,它的成本只不过是用于存储它的四个字节的RAM。 谨防Windows术语:在Win32中,我称之为互斥锁被视为“互锁区域”; Win32在调用CreateMutex()时创建的东西是完全不同的,可以从几个不同的进程访问,并且由于它涉及到内核的往返,所以要昂贵得多。 请注意,在Java中,每个对象实例都包含一个互斥锁,而Java开发人员似乎并没有对该主题过于脾气暴躁。

一种更简单的解决方案的可能性是在整个缓存上使用单个读取器/写入器锁。 鉴于您知道存在最大条目数(并且它相对较小),听起来像向缓存添加新密钥是一种“罕见”事件。 一般逻辑是:

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

我不知道有关使用模式的更多信息,我不能肯定这是否是一个很好的解决方案。 但是,它非常简单,如果使用主要是读取,那么在锁定方面它非常便宜。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM