首页 > 解决方案 > std::shared_timed_mutex 何时比 std::mutex 慢,何时(不)使用它?

问题描述

我正在尝试使用本文作为提示或灵感在 C++ 中实现多线程 LRU缓存。它适用于 Go,但所需的概念或多或少也存在于 C++ 中。本文建议在散列表和链表周围使用具有共享互斥锁的细粒度锁定。

所以我打算用 写一个缓存std::unordered_mapstd::list并用std::shared_timed_mutex. 我的用例包括几个线程 (4-8) 使用此缓存作为拼写错误单词的存储和相应的可能更正。缓存的大小约为 10000-100000 个项目。

但是我在几个地方读到,使用共享互斥体而不是普通互斥体几乎没有意义,而且速度较慢,尽管我找不到一些带有数字的真正基准,或者至少是模糊的指导方针,何时使用和何时不使用共享互斥体。而其他来源建议在任何时候使用共享互斥锁,只要您有或多或少超过并发作者的并发读者。

  1. 什么时候使用 anstd::shared_timed_mutex比使用 plain更好std::mutex?阅读者/阅读者应该多于作者/写入者多少次?当然我知道这取决于很多因素,但是我应该如何决定使用哪一个呢?
  2. 也许它依赖于平台,并且某些平台实现比其他平台更差?(我们使用 Linux 和 Windows 作为目标,MSVC 2017 和 GCC 5)
  3. 如文章中所述实现缓存锁定是否有意义?
  4. std::shared_mutex与定时相比,(来自 C++17)在性能上有什么不同吗?

PS我觉得会有“首先测量/配置最适合你的情况”。我会,但我需要先实现一个,如果存在一些启发式方法可供选择,而不是同时实现选项和测量,那就太好了。此外,即使我测量,我认为结果将取决于我使用的数据。并且很难预测真实数据(例如,对于云中的服务器)。

标签: c++multithreadingc++14

解决方案


  1. 什么时候使用 anstd::shared_timed_mutex比使用 plain更好std::mutex?阅读者/阅读者应该多于作者/写入者多少次?当然我知道这取决于很多因素,但是我应该如何决定使用哪一个呢?

由于它们的额外复杂性,读/写锁(std::shared_mutex, std::shared_timed_mutex)优于普通锁(std::mutex, std::timed_mutex)的情况很少见。它们确实存在,但就个人而言,我自己从未遇到过。

如果您有频繁但短暂的读取操作,则读/写互斥锁不会提高性能。它更适合读取操作频繁且昂贵的场景。当读取操作只是在内存数据结构中的查找时,很可能一个简单的锁将胜过读/写解决方案。

如果读取操作非常昂贵并且您可以并行处理许多操作,那么增加读取与写入比率应该会在某些时候导致读取/写入器将优于排他锁的情况。临界点在哪里取决于实际工作量。我不知道一个好的经验法则。

另请注意,在持有锁的同时执行昂贵的操作通常是一个不好的迹象。可能有比使用读/写锁更好的方法来解决问题。

比我在该领域有更多知识的人对该主题的两条评论:

  • Howard Hinnant 的回答C++14 shared_timed_mutex VS C++11 mutex
  • Anthony Williams 的引用可以在这个答案的末尾找到(不幸的是,这个原始帖子的链接似乎已经死了)。他解释了为什么读/写锁很慢,而且通常不是理想的解决方案。
  1. 也许它依赖于平台,并且某些平台实现比其他平台更差?(我们使用 Linux 和 Windows 作为目标,MSVC 2017 和 GCC 5)

我不知道操作系统之间的显着差异。我的期望是情况会相似。在 Linux 上,GCC 库依赖于 glibc 的读/写锁实现。如果你想深入研究,你可以在pthread_rwlock_common.c中找到实现。它还说明了读/写锁带来的额外复杂性。

Boost中的实现存在一个老问题shared_mutex#11798 - 在 POSIX 上实现 boost::shared_mutex 是次优的)。但是我不清楚实现是否可以改进,或者它只是一个不太适合读/写锁的例子。

  1. 如文章中所述实现缓存锁定是否有意义?

坦率地说,我怀疑读/写锁会提高这种数据结构的性能。阅读器操作应该非常快,因为它只是一个查找。更新 LRU 列表也发生在读取操作之外(在 Go 实现中)。

一个实现细节。在这里使用链表并不是一个坏主意,因为它使更新操作非常快(您只需更新指针)。使用时std::list请记住,它通常涉及内存分配,当您持有密钥时应该避免这种情况。最好在获取锁之前分配内存,因为内存分配很昂贵。

在他们的 HHVM 项目中,Facebook 有并发 LRU 缓存的 C++ 实现,看起来很有希望:

  1. 并发LRUCache
  2. 并发可扩展缓存

LRU 列表和映射本身(英特尔的并发哈希映射实现)也ConcurrentLRUCache使用链表(但不是)。请注意,对于 LRU 列表更新的锁定,他们没有像 Go 实现中那样采用读/写方法,而是使用简单的排他锁。std::listtbb::concurrent_hash_mapstd::mutex

第二个实现 ( ConcurrentScalableCache) 建立在ConcurrentLRUCache. 他们使用分片来提高可扩展性。缺点是 LRU 属性只是近似值(取决于您使用的分片数量)。在某些可能会降低缓存命中率的工作负载中,这是一个很好的技巧,可以避免所有操作必须共享相同的锁。

  1. 与定时相比,std::shared_mutex(来自 C++17)在性能上有什么不同吗?

我没有关于开销的基准数字,但它看起来像是比较苹果和橘子。如果您需要计时功能,您别无选择,只能使用std::shared_timed_mutex. 但是如果你不需要它,你可以简单地使用std::shared_mutex,它必须做更少的工作,因此永远不会变慢。

对于需要超时的典型场景,我不认为时间开销太严重,因为无论如何在这种情况下锁往往会保持更长时间。但正如所说,我不能用真实的测量来支持这一说法。


推荐阅读