c++ - std::shared_timed_mutex 何时比 std::mutex 慢,何时(不)使用它?
问题描述
我正在尝试使用本文作为提示或灵感在 C++ 中实现多线程 LRU缓存。它适用于 Go,但所需的概念或多或少也存在于 C++ 中。本文建议在散列表和链表周围使用具有共享互斥锁的细粒度锁定。
所以我打算用 写一个缓存std::unordered_map
,std::list
并用std::shared_timed_mutex
. 我的用例包括几个线程 (4-8) 使用此缓存作为拼写错误单词的存储和相应的可能更正。缓存的大小约为 10000-100000 个项目。
但是我在几个地方读到,使用共享互斥体而不是普通互斥体几乎没有意义,而且速度较慢,尽管我找不到一些带有数字的真正基准,或者至少是模糊的指导方针,何时使用和何时不使用共享互斥体。而其他来源建议在任何时候使用共享互斥锁,只要您有或多或少超过并发作者的并发读者。
- 什么时候使用 an
std::shared_timed_mutex
比使用 plain更好std::mutex
?阅读者/阅读者应该多于作者/写入者多少次?当然我知道这取决于很多因素,但是我应该如何决定使用哪一个呢? - 也许它依赖于平台,并且某些平台实现比其他平台更差?(我们使用 Linux 和 Windows 作为目标,MSVC 2017 和 GCC 5)
- 如文章中所述实现缓存锁定是否有意义?
std::shared_mutex
与定时相比,(来自 C++17)在性能上有什么不同吗?
PS我觉得会有“首先测量/配置最适合你的情况”。我会,但我需要先实现一个,如果存在一些启发式方法可供选择,而不是同时实现选项和测量,那就太好了。此外,即使我测量,我认为结果将取决于我使用的数据。并且很难预测真实数据(例如,对于云中的服务器)。
解决方案
- 什么时候使用 an
std::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 的引用可以在这个答案的末尾找到(不幸的是,这个原始帖子的链接似乎已经死了)。他解释了为什么读/写锁很慢,而且通常不是理想的解决方案。
- 也许它依赖于平台,并且某些平台实现比其他平台更差?(我们使用 Linux 和 Windows 作为目标,MSVC 2017 和 GCC 5)
我不知道操作系统之间的显着差异。我的期望是情况会相似。在 Linux 上,GCC 库依赖于 glibc 的读/写锁实现。如果你想深入研究,你可以在pthread_rwlock_common.c中找到实现。它还说明了读/写锁带来的额外复杂性。
Boost中的实现存在一个老问题shared_mutex
(#11798 - 在 POSIX 上实现 boost::shared_mutex 是次优的)。但是我不清楚实现是否可以改进,或者它只是一个不太适合读/写锁的例子。
- 如文章中所述实现缓存锁定是否有意义?
坦率地说,我怀疑读/写锁会提高这种数据结构的性能。阅读器操作应该非常快,因为它只是一个查找。更新 LRU 列表也发生在读取操作之外(在 Go 实现中)。
一个实现细节。在这里使用链表并不是一个坏主意,因为它使更新操作非常快(您只需更新指针)。使用时std::list
请记住,它通常涉及内存分配,当您持有密钥时应该避免这种情况。最好在获取锁之前分配内存,因为内存分配很昂贵。
在他们的 HHVM 项目中,Facebook 有并发 LRU 缓存的 C++ 实现,看起来很有希望:
LRU 列表和映射本身(英特尔的并发哈希映射实现)也ConcurrentLRUCache
使用链表(但不是)。请注意,对于 LRU 列表更新的锁定,他们没有像 Go 实现中那样采用读/写方法,而是使用简单的排他锁。std::list
tbb::concurrent_hash_map
std::mutex
第二个实现 ( ConcurrentScalableCache
) 建立在ConcurrentLRUCache
. 他们使用分片来提高可扩展性。缺点是 LRU 属性只是近似值(取决于您使用的分片数量)。在某些可能会降低缓存命中率的工作负载中,这是一个很好的技巧,可以避免所有操作必须共享相同的锁。
- 与定时相比,std::shared_mutex(来自 C++17)在性能上有什么不同吗?
我没有关于开销的基准数字,但它看起来像是比较苹果和橘子。如果您需要计时功能,您别无选择,只能使用std::shared_timed_mutex
. 但是如果你不需要它,你可以简单地使用std::shared_mutex
,它必须做更少的工作,因此永远不会变慢。
对于需要超时的典型场景,我不认为时间开销太严重,因为无论如何在这种情况下锁往往会保持更长时间。但正如所说,我不能用真实的测量来支持这一说法。
推荐阅读
- android-studio - 在android studio中删除AVD后如何释放空间?
- python-3.x - 如何使用python将长音频(EX:1小时)文件拆分为多个短长度(5s)音频文件
- python - 如何使用 selenium 和 python 在谷歌驱动器中上传文件?
- azure-functions - Azure 函数返回 IQueryable
- laravel - 在十月 CMS 中创建前端记录过滤器
- typescript - 带有 TypeScript 的 ExpressJs - 在中间件之间传递数据
- javascript - 是否可以通过另一个对象的属性对对象数组进行排序?
- facebook - 从 PHP 网站获取 Facebook 页面的帖子
- ocr - 是否可以使用 IronOCR 从该图像中获取 4 个数字?
- javascript - 试图在 Flask 框架中调用 HTML 中的 JavaScript