我试图在C++中实现多线程LRU缓存,使用这文章作为提示或启发。这是为了Go,但在C++中也或多或少地存在所需的概念。本文建议在哈希表和链接列表周围使用共享互斥锁的细粒度锁定。
因此,我打算使用std::unordered_map、std::list编写缓存,并使用std::shared_timed_mutex进行锁定。我的用例包括几个线程(4-8),使用这个缓存存储拼写错误的单词和相应的可能更正。缓存的大小将在10000到100000项左右。
但我在几个地方读到,使用共享互斥而不是普通互斥是很少有意义的,而且速度更慢,尽管我找不到一些有数字的真正基准,或者至少是模糊的指南,什么时候使用,什么时候不用共享互斥。尽管其他来源建议在任何时候使用共享互斥对象,但并发读取器的数量或多或少超过了并发作者。
std::shared_timed_mutex比普通的std::mutex更好?读者/阅读数量应该超过作者/写作多少次?当然,这取决于许多因素,但我应该如何决定使用哪一种呢?std::shared_mutex相比,C++17的性能有什么不同吗?P.S.我觉得会有“最适合你的情况的第一次测量/分析”。我会的,但我需要先实现一个,如果有一些启发来选择,而不是同时实现选项和度量,那就太好了。而且,即使我测量,我认为结果将取决于我使用的数据。而且很难预测真实的数据(例如,对于云中的服务器)。
发布于 2019-05-16 00:59:29
std::shared_timed_mutex比普通的std::mutex更好?读者/阅读数量应该超过作者/写作多少次?当然,这取决于许多因素,但我应该如何决定使用哪一种呢?由于其额外的复杂性,读/写锁(std::shared_mutex、std::shared_timed_mutex)优于普通锁(std::mutex、std::timed_mutex)的情况很少。它们确实存在,但就我个人而言,我自己从未遇到过。
读/写互斥将不会提高性能,如果您有频繁,但短期阅读操作。它更适合于被读取的场景操作频繁和昂贵。当读取操作只是内存数据结构中的查找时,一个简单的锁很可能会优于读/写解决方案。
如果读取操作非常昂贵,而且您可以并行处理许多操作,那么增加读/写比应该会在某一时刻导致读/写操作优于独占锁的情况。这个临界点的位置取决于实际的工作负载。我不知道有什么好的经验法则。
还要注意的是,在保持锁的同时执行昂贵的操作通常是个坏兆头。可能有更好的方法来解决这个问题,然后使用读/写锁。
在这个问题上,比我更了解这个领域的人对这个话题有两点评论:
我不知道操作系统之间有很大的区别。我的期望是情况会类似。在Linux上,GCC库依赖于glibc的读/写锁实现。如果您想深入研究,可以在common.c中找到实现。它还说明了读/写锁带来的额外复杂性。
Boost中的shared_mutex实现(POSIX上的互斥是次优的)存在一个老问题。但是,我不清楚该实现是否可以改进,或者它是否只是一个不适合读/写锁的示例。
坦率地说,我怀疑读/写锁在这样的数据结构中会提高性能。读取器操作应该非常快,因为它只是一个查找。更新LRU列表也发生在读取操作之外(在Go实现中)。
一个实现细节。在这里使用链接列表并不是个坏主意,因为它使更新操作非常快(您只需更新指针)。在使用std::list时,请记住,它通常涉及内存分配,当您持有密钥时,您应该避免这样做。最好在获得锁之前分配内存,因为内存分配非常昂贵。
在他们的HHVM项目中,Facebook有并发LRU缓存的C++实现,看起来很有希望:
ConcurrentLRUCache还对LRU列表使用链接列表(但不使用std::list),对于映射本身使用tbb::concurrent_hash_map (来自英特尔的并发散列映射实现)。注意,对于LRU列表更新的锁定,它们不像go实现那样采用读/写方法,而是使用一个简单的std::mutex独占锁。
第二个实现(ConcurrentScalableCache)构建在ConcurrentLRUCache之上。它们使用切分来提高可伸缩性。缺点是LRU属性只是近似的(取决于您使用的碎片数量)。在某些工作负载中,可能会降低缓存命中率,但避免所有操作都必须共享相同的锁是一个很好的技巧。
我没有关于开销的基准数字,但看上去像是比较苹果和橘子。如果您需要定时功能,那么除了使用std::shared_timed_mutex之外,您别无选择。但是,如果您不需要它,您可以简单地使用std::shared_mutex,它所做的工作要少一些,因此永远不要慢。
当您需要超时时,对于典型的场景,我不会期望时间开销太严重,因为在这种情况下,锁的持有时间往往会更长。但如前所述,我不能用实际的测量来支持这一说法。
发布于 2020-10-28 14:47:07
因此,哪些实际问题可以通过std::shared_mutex来解决。
让我们想象一下你正在写一些实时音频软件。您有一些回调,由驱动程序每秒调用1000次,并且您必须将1ms的音频数据放入其缓冲区,让硬件在接下来的1ms中播放它。并且您有“大”的音频数据缓冲区(例如10秒),它由其他线程在后台呈现,每10秒写一次。另外,还有10个线程希望从同一个缓冲区读取数据(在UI上绘制一些东西,通过网络发送,控制外部灯等等)。这是真正的DJ软件的真正任务,不是玩笑。
因此,在每次回调调用(每1ms)时,您都有非常低的机会与编写线程发生冲突(0.01%),但您几乎有100%的机会与另一个读者线程发生冲突-它们一直在工作,并且读取相同的缓冲区!所以,假设某个线程从这个被锁定的std::mutex中读取数据,并决定通过网络发送一些东西,然后等待下一个500 ms的响应--您将被锁定,什么也做不了,您的硬件将无法获得下一部分声音,并且它将播放静音(例如,想象一下这是一场音乐会)。这是场灾难。
但是这里有一个解决方案--对所有的读取器线程使用std::shared_mutex和std::shared_lock。是的,std::shared_lock的平均锁会花费你更多(假设不是50纳秒,而是100纳秒--即使对你的实时应用程序来说,这仍然非常便宜,它应该在最大1毫秒内写缓冲区),但是当另一个阅读器线程锁定你的性能时,你是100%安全的--这对你的性能至关重要的线程是500毫秒。
这就是使用std::shared_mutex的原因--避免/改善最坏的情况。而不是提高平均性能(这应该以其他一些方式实现)。
https://stackoverflow.com/questions/50972345
复制相似问题