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

时间:2010-01-26 13:46:42

标签: c++ caching thread-safety

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

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

    主要限制因素是:

    1. 永远不要同时计算密钥的值。
    2. 可以同时计算2个不同键的值。
    3. 数据检索不得锁定其他线程从其他密钥检索数据。
    4. 我已解决的其他限制是:

      1. 修复(在编译时已知)最大缓存大小,基于MRU(最近使用)抖动。
      2. 通过引用检索(暗示互斥共享计数)
      3. 我不确定每个密钥使用1个互斥锁是实现此目的的正确方法,但我没有找到任何其他实质上不同的方式。

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

3 个答案:

答案 0 :(得分:3)

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

答案 1 :(得分:2)

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

我建议如下:

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

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

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

    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()且{{1}是signalpthread_cond_signal()以原子方式释放互斥锁并将线程标记为等待条件;线程被唤醒时,会自动重新获取互斥锁。

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

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

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

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

答案 2 :(得分:0)

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

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

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