首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >什么时候std::shared_timed_mutex比std::mutex慢,什么时候(不)使用它?

什么时候std::shared_timed_mutex比std::mutex慢,什么时候(不)使用它?
EN

Stack Overflow用户
提问于 2018-06-21 15:31:03
回答 2查看 2.8K关注 0票数 8

我试图在C++中实现多线程LRU缓存,使用文章作为提示或启发。这是为了Go,但在C++中也或多或少地存在所需的概念。本文建议在哈希表和链接列表周围使用共享互斥锁的细粒度锁定。

因此,我打算使用std::unordered_mapstd::list编写缓存,并使用std::shared_timed_mutex进行锁定。我的用例包括几个线程(4-8),使用这个缓存存储拼写错误的单词和相应的可能更正。缓存的大小将在10000到100000项左右。

但我在几个地方读到,使用共享互斥而不是普通互斥是很少有意义的,而且速度更慢,尽管我找不到一些有数字的真正基准,或者至少是模糊的指南,什么时候使用,什么时候不用共享互斥。尽管其他来源建议在任何时候使用共享互斥对象,但并发读取器的数量或多或少超过了并发作者。

  1. 什么时候使用std::shared_timed_mutex比普通的std::mutex更好?读者/阅读数量应该超过作者/写作多少次?当然,这取决于许多因素,但我应该如何决定使用哪一种呢?
  2. 也许它是依赖于平台的,有些平台实现比其他平台更糟糕?(我们以Linux和Windows为目标,MSVC 2017和GCC 5)
  3. 如本文所述,实现缓存锁定是否有意义?
  4. 与定时的std::shared_mutex相比,C++17的性能有什么不同吗?

P.S.我觉得会有“最适合你的情况的第一次测量/分析”。我会的,但我需要先实现一个,如果有一些启发来选择,而不是同时实现选项和度量,那就太好了。而且,即使我测量,我认为结果将取决于我使用的数据。而且很难预测真实的数据(例如,对于云中的服务器)。

EN

回答 2

Stack Overflow用户

发布于 2019-05-16 00:59:29

  1. 什么时候使用std::shared_timed_mutex比普通的std::mutex更好?读者/阅读数量应该超过作者/写作多少次?当然,这取决于许多因素,但我应该如何决定使用哪一种呢?

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

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

如果读取操作非常昂贵,而且您可以并行处理许多操作,那么增加读/写比应该会在某一时刻导致读/写操作优于独占锁的情况。这个临界点的位置取决于实际的工作负载。我不知道有什么好的经验法则。

还要注意的是,在保持锁的同时执行昂贵的操作通常是个坏兆头。可能有更好的方法来解决这个问题,然后使用读/写锁。

在这个问题上,比我更了解这个领域的人对这个话题有两点评论:

  • 霍华德·辛南特的回答互斥对C++11互斥
  • 安东尼·威廉姆斯的话可以在这个答案的末尾找到(不幸的是,这个原始帖子的链接似乎已经死了)。他解释了为什么读/写锁速度慢,而且往往不是理想的解决方案。

  1. 也许它是依赖于平台的,有些平台实现比其他平台更糟糕?(我们以Linux和Windows为目标,MSVC 2017和GCC 5)

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

Boost中的shared_mutex实现(POSIX上的互斥是次优的)存在一个老问题。但是,我不清楚该实现是否可以改进,或者它是否只是一个不适合读/写锁的示例。

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

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

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

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

  1. ConcurrentLRUCache
  2. ConcurrentScalableCache

ConcurrentLRUCache还对LRU列表使用链接列表(但不使用std::list),对于映射本身使用tbb::concurrent_hash_map (来自英特尔的并发散列映射实现)。注意,对于LRU列表更新的锁定,它们不像go实现那样采用读/写方法,而是使用一个简单的std::mutex独占锁。

第二个实现(ConcurrentScalableCache)构建在ConcurrentLRUCache之上。它们使用切分来提高可伸缩性。缺点是LRU属性只是近似的(取决于您使用的碎片数量)。在某些工作负载中,可能会降低缓存命中率,但避免所有操作都必须共享相同的锁是一个很好的技巧。

  1. 与定时的std::shared_mutex相比,C++17的性能有什么不同吗?

我没有关于开销的基准数字,但看上去像是比较苹果和橘子。如果您需要定时功能,那么除了使用std::shared_timed_mutex之外,您别无选择。但是,如果您不需要它,您可以简单地使用std::shared_mutex,它所做的工作要少一些,因此永远不要慢。

当您需要超时时,对于典型的场景,我不会期望时间开销太严重,因为在这种情况下,锁的持有时间往往会更长。但如前所述,我不能用实际的测量来支持这一说法。

票数 8
EN

Stack Overflow用户

发布于 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的原因--避免/改善最坏的情况。而不是提高平均性能(这应该以其他一些方式实现)。

票数 2
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/50972345

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档