【问题标题】:What makes the boost::shared_mutex so slow是什么让 boost::shared_mutex 这么慢
【发布时间】:2019-11-07 20:41:48
【问题描述】:

我使用 google benchmark 运行了以下 3 次测试,结果让我感到惊讶,因为 RW 锁在发布模式下比简单互斥锁慢约 4 倍。 (并且在调试模式下比简单的互斥锁慢约 10 倍)

void raw_access() {
    (void) (gp->a + gp->b);
}

void mutex_access() {
    std::lock_guard<std::mutex> guard(g_mutex);
    (void) (gp->a + gp->b);
}

void rw_mutex_access() {
    boost::shared_lock<boost::shared_mutex> l(g_rw_mutex);
    (void) (gp->a + gp->b);
}

结果是:

2019-06-26 08:30:45
Running ./perf
Run on (4 X 2500 MHz CPU s)
CPU Caches:
  L1 Data 32K (x2)
  L1 Instruction 32K (x2)
  L2 Unified 262K (x2)
  L3 Unified 4194K (x1)
Load Average: 5.35, 3.22, 2.57
-----------------------------------------------------------
Benchmark                 Time             CPU   Iterations
-----------------------------------------------------------
BM_RawAccess           1.01 ns         1.01 ns    681922241
BM_MutexAccess         18.2 ns         18.2 ns     38479510
BM_RWMutexAccess       92.8 ns         92.8 ns      7561437

我没有通过谷歌获得足够的信息,所以希望在这里得到一些帮助。

谢谢

【问题讨论】:

  • 嗯,共享互斥锁要复杂得多……这不是预期的结果吗?另外,您正在将 std::mutexboost::shared_mutex 进行比较,请尝试改用 std::shared_mutex
  • 感谢@CruzJean 提供 std::shared_mutex。成本从 96ns 降低到 38ns。 “这不是预期的结果吗?”实际上,我想要一些有关复杂性的更多信息。现在 std::shared_mutex 和 std::mutex 只有 2 倍的差异,所以可以预期。

标签: c++ boost boost-thread


【解决方案1】:

我不知道标准库/增强/等的细节。实现有所不同,尽管标准库版本似乎更快(恭喜,无论是谁编写的)

因此,我将尝试从理论上解释各种互斥锁类型之间的速度差异,这将解释为什么共享互斥锁(应该)会更慢。

原子自旋锁

作为一个学术练习,考虑一下最简单的线程安全“类互斥锁”实现:简单的原子自旋锁。

本质上,这只不过是std::atomic&lt;bool&gt;std::atomic_flag。它被初始化为假。要“锁定”互斥锁,您只需在循环中执行原子比较和交换操作,直到获得错误值(即,在原子将其设置为 true 之前,前一个值是 false)。

std::atomic_flag flag = ATOMIC_FLAG_INIT;

// lock it by looping until we observe a false value
while (flag.test_and_set()) ;

// do stuff under "mutex" lock

// unlock by setting it back to false state
flag.clear();

但是,由于这种构造的性质,它就是我们所说的“不公平”互斥体,因为获取锁的线程的顺序不一定是它们开始尝试锁定它的顺序。也就是说,在高竞争下,一个线程可能会尝试锁定并且永远不会成功,因为其他线程更幸运。它对时间非常敏感。想象一下音乐椅。

因此,虽然它的功能类似于互斥锁,但它并不是我们认为的“互斥锁”。

互斥体

可以将互斥锁视为建立在原子自旋锁之上(尽管通常不会这样实现,因为它们通常是在操作系统和/或硬件的支持下实现的)。

本质上,互斥锁比原子自旋锁高出一步,因为它有一个等待线程队列。这让它变得“公平”,因为锁定获取的顺序(或多或少)与锁定尝试的顺序相同。

如果您已经注意到,如果您运行sizeof(std::mutex),它可能会比您预期的要大一些。在我的平台上它是 40 字节。该额外空间用于保存状态信息,特别是包括访问每个互斥体的锁定队列的某种方式。

当您尝试锁定互斥体时,它会执行一些低级别的线程安全操作以线程安全地访问互斥体的状态信息(例如原子自旋锁),检查互斥体的状态,将您的线程添加到锁定队列,并且(通常)在您等待时让您的线程进入睡眠状态,这样您就不会消耗宝贵的 CPU 时间。低级线程安全操作(例如原子自旋锁)在线程进入睡眠状态的同时自动释放(这通常是操作系统或硬件支持需要高效的地方)。

解锁是通过执行低级线程安全操作(例如原子自旋锁)、从队列中弹出下一个等待线程并唤醒它来执行的。被唤醒的线程现在“拥有”锁。冲洗并重复。

共享互斥体

共享互斥体使这一概念更进一步。它可以由单个线程拥有以获取读/写权限(如普通互斥锁),或由多个线程拥有以获取只读权限(无论如何,这取决于程序员确保它的安全性)。

因此,除了唯一的所有权队列(如普通互斥体)外,它还具有共享所有权状态。共享所有权状态可以只是对当前具有共享所有权的线程数的计数。如果您检查sizeof(std::shared_mutex),您会发现它通常比std::mutex 还要大。例如,在我的系统上,它是 56 个字节。

所以当你去锁定一个共享互斥体时,它必须做一个普通互斥体所做的一切,但还必须验证一些其他的东西。例如,如果您尝试唯一锁定,则必须验证没有共享所有者。当您尝试共享锁定时,它必须验证没有唯一所有者。

因为我们通常希望互斥锁是“公平的”,所以一旦唯一的锁在队列中,未来的共享锁尝试必须排队而不是获取锁,即使它当前可能处于共享(即非唯一)锁通过几个线程。这是为了确保共享所有者不会“欺负”需要唯一所有权的线程。

但这也反过来:排队逻辑必须确保共享锁在共享所有权期间永远不会放入空队列(因为它应该立即成功并成为另一个共享所有者)。

此外,如果有一个独特的储物柜,然后是一个共享储物柜,然后是一个独特的储物柜,它必须(大致)保证获取顺序。所以锁队列中的每个条目还需要一个标志来表示其用途(即共享与唯一)。

然后我们想到唤醒逻辑。解锁共享互斥锁时,逻辑会根据互斥锁的当前所有权类型而有所不同。如果解锁线程具有唯一所有权或者是最后一个共享所有者,它可能必须从队列中唤醒一些线程。它将唤醒队列前面请求共享所有权的所有线程,或者唤醒队列前面请求唯一所有权的单个线程。

您可以想象,所有这些关于谁因什么原因锁定以及它如何变化的额外逻辑不仅取决于当前所有者,还取决于队列的内容,这可能会导致速度变慢。希望您的阅读频率明显高于写入频率,因此您可以同时运行多个共享所有者,从而减轻协调所有这些对性能的影响。

【讨论】:

    猜你喜欢
    • 2011-11-14
    • 1970-01-01
    • 2013-08-21
    • 2012-05-01
    • 2023-03-29
    • 2011-06-07
    • 2020-05-14
    • 2011-05-21
    • 2021-12-03
    相关资源
    最近更新 更多