- 什么时候使用
std::shared_timed_mutex
比普通的 std::mutex
更好?读者/读取次数应该超过写入者/写入次数多少倍?当然,我知道这取决于许多因素,但是我应该如何做出决策选择哪个锁?
由于其额外的复杂性,相对于普通锁(std::mutex
,std::timed_mutex
),读写锁(std::shared_mutex
,std::shared_timed_mutex
)更优越的情况很少见。它们确实存在,但就我个人而言,我从未遇到过这种情况。
如果您经常进行短时间读取操作,则读/写互斥锁不会提高性能。它更适用于读操作频繁且昂贵的场景。当读操作仅是内存数据结构中的查找时,简单的锁很可能优于读/写解决方案。
如果读操作非常昂贵并且您可以同时处理多个读操作,则增加读与写的比率应该会导致读/写锁在某些情况下优于独占锁。这个临界点取决于实际工作负载,我不知道有没有好的经验法则。
此外,请注意,在持有锁时执行昂贵的操作通常是一个不好的标志。可能有更好的方法来解决问题,而不是使用读/写锁。
以下是两位比我更了解该领域的人对此主题的评论:
- 也许这取决于平台,有些平台的实现比其他平台更差?(我们将Linux和Windows作为目标,使用MSVC 2017和GCC 5)
我不知道操作系统之间有显著的差异。 我期望情况将是类似的。 在Linux上,GCC库依赖于glibc的读/写锁实现。 如果您想深入了解,可以在
pthread_rwlock_common.c中找到实现。 它还说明了使用读/写锁带来的额外复杂性。
Boost中的
shared_mutex
实现存在一个旧问题(
#11798 - Implementation of boost::shared_mutex on POSIX is suboptimal)。 但我不确定实现是否可以改进,或者它只是一个不适合读/写锁的例子。
- 按照文章所述实现缓存锁定是否有意义?
坦白说,我对于在这样的数据结构中使用读/写锁来提高性能持怀疑态度。读操作应该非常快,因为它只是一个查找操作。更新LRU列表也发生在读操作之外(在Go实现中)。
一个实现细节。在这里使用链表并不是一个坏主意,因为它使得更新操作非常快(你只需要更新指针)。当使用
std::list
时,请记住它通常涉及内存分配,而当你拥有关键字时应避免这种情况。最好在获取锁之前分配内存,因为内存分配是昂贵的。
在他们的HHVM项目中,Facebook有一些看起来很有前途的并发LRU缓存的C++实现:
- ConcurrentLRUCache并发LRU缓存
- ConcurrentScalableCache并发可扩展缓存
ConcurrentLRUCache
还使用了一个链表(但不是std::list
)作为LRU列表,并且使用Intel的tbb::concurrent_hash_map
作为映射本身的实现(一种并发哈希映射实现)。请注意,对于LRU列表更新的锁定,他们没有采用Go实现中的读/写器方法,而是使用了一个简单的std::mutex
独占锁。
第二个实现(
ConcurrentScalableCache
)基于
ConcurrentLRUCache
。它们使用分片来提高可伸缩性。缺点是LRU属性只是近似的(取决于使用多少个分片)。在某些工作负载中,这可能会降低缓存命中率,但这是一个很好的技巧,可以避免所有操作都必须共享同一个锁。
- 与定时锁相比,std::shared_mutex(来自C++17)在性能上有什么区别吗?
我没有关于开销的基准数字,但看起来像是在比较苹果和橙子。如果您需要计时功能,则除了使用std::shared_timed_mutex
之外别无选择。但是,如果您不需要它,则可以简单地使用std::shared_mutex
,因为它需要做更少的工作,因此永远不应该更慢。
对于需要超时的典型场景,我不希望计时开销太严重,因为在这种情况下锁 tend to be hold longer. 但是,正如我所说,我无法用真实的测量数据支持这个说法。