9

在 Visual Studio 2022 中以发布模式运行以下内容:

#include <chrono>
#include <mutex>
#include <shared_mutex>
#include <iostream>

std::mutex mx;
std::shared_mutex smx;

constexpr int N = 100'000'000;

int main()
{
    auto t1 = std::chrono::steady_clock::now();
    for (int i = 0; i != N; i++)
    {
        std::unique_lock<std::mutex> l{ mx };
    }
    auto t2 = std::chrono::steady_clock::now();
    for (int i = 0; i != N; i++)
    {
        std::unique_lock<std::shared_mutex> l{ smx };
    }
    auto t3 = std::chrono::steady_clock::now();

    auto d1 = std::chrono::duration_cast<std::chrono::duration<double>>(t2 - t1);
    auto d2 = std::chrono::duration_cast<std::chrono::duration<double>>(t3 - t2);

    std::cout << "mutex " << d1.count() << "s;  shared_mutex " << d2.count() << "s\n";
    std::cout << "mutex " << sizeof(mx) << " bytes;  shared_mutex " << sizeof(smx) << " bytes \n";
}

输出如下:

mutex 2.01147s;  shared_mutex 1.32065s
mutex 80 bytes;  shared_mutex 8 bytes

为什么这样?

出乎意料的是,特征更丰富的std::shared_mutex速度比std::mutex严格来说是其特征的一个子集。

4

1 回答 1

15

TL;DR:不幸的是,向后兼容性和 ABI 兼容性问题std::mutex的结合会导致下一次 ABI 中断。OTOH,std::shared_mutex很好。


一个体面的实现std::mutex会尝试使用原子操作来获取锁,如果忙,可能会尝试在读取循环中旋转(pause在 x86 上有一些),最终会求助于操作系统等待。

有几种方法可以实现std::mutex

  1. 直接委托给执行上述所有操作的相应操作系统 API。
  2. 自己做旋转和原子的事情,只为操作系统等待调用操作系统 API。

当然,第一种方式更容易实现,调试更友好,更健壮。所以这似乎是要走的路。候选 API 是:

  • CRITICAL_SECTION蜜蜂。递归互斥体,缺少静态初始化程序,需要显式销毁
  • SRWLOCK. 具有静态初始化程序且不需要显式销毁的非递归共享互斥锁
  • WaitOnAddress. 等待特定变量更改的 API,类似于 Linux futex

这些原语具有操作系统版本要求:

  • CRITICAL_SECTION自从我认为 Windows 95 就存在了,虽然TryEnterCriticalSection在 Windows 9x 中不存在,但是自 Windows Vista 以来添加了使用CRITICAL_SECTIONwith的能力,with本身。CONDITION_VARIABLECONDITION_VARIABLE
  • SRWLOCK从 Windows Vista 开始TryAcquireSRWLockExclusive存在,但从 Windows 7 开始存在,所以只能std::mutex在 Windows 7 中直接实现启动。
  • WaitOnAddress从 Windows 8 开始添加。

添加的时候std::mutex,需要Visual Studio C++库对Windows XP的支持,所以它是使用自己的服务来实现的。事实上,std::mutex其他同步内容已委托给 ConCRT(并发运行时

对于 Visual Studio 2015,实现已切换为使用最佳可用机制,即SRWLOCK从 Windows 7 开始,并CRITICAL_SECTION在 Windows Vista 中声明。ConCRT 被证明不是最好的机制,但它仍然用于 Windows XP 和 2003。多态性是通过将具有虚函数的新类放置到由std::mutex其他原语提供的缓冲区中来实现的。

请注意,由于运行时检测、新放置以及 Window 7 之前的实现无法仅具有静态初始化程序,此实现打破了std::mutex对 be的要求。constexpr

随着时间的推移,对 Windows XP 的支持最终在 VS 2019 中被放弃,对 Windows Vista 的支持在 VS 2022 中被放弃,进行更改是为了避免使用 ConCRT,计划更改甚至避免运行时检测 SRWLOCK(披露:I'我贡献了这些 PR)。仍然由于 VS 2015 与 VS 2022 的 ABI 兼容性,不可能简化std::mutex实现以避免所有这些将类与虚函数一起放置。

更可悲的是,虽然SRWLOCK有静态初始化程序,但所述兼容性阻止了constexpr互斥锁:我们必须在那里放置新的实现。不可能避免放置 new 并在内部进行构建std::mutex,因为std::mutex必须是标准布局类(请参阅为什么 std::mutex 是标准布局类?)。

所以大小开销来自于 ConCRT 互斥体的大小。

运行时开销来自调用链:

  • 库函数调用以获取标准库实现
  • 虚函数调用 get toSRWLOCK基于 - 的实现
  • 最后是 Windows API 调用。

由于标准库 DLL 是用/guard:cf.

运行时开销的一部分是由于std::mutex填充所有权计数和锁定线程。尽管SRWLOCK. 这是由于与recursive_mutex. 额外的信息可能对调试有帮助,但填写起来确实需要时间。


std::shared_mutex旨在仅支持启动 Windows 7 的系统。因此它SRWLOCK直接使用。

的大小std::shared_mutex是 的大小SRWLOCKSRWLOCK具有与指针相同的大小(尽管在内部它不是指针)。

它仍然涉及一些可避免的开销:它调用 C++ 运行时库,只是为了调用 Windows API,而不是直接调用 Windows API。不过,这看起来可以通过下一个 ABI 修复。

std::shared_mutex构造函数可以是 constexpr,因为SRWLOCK不需要动态初始化器,但标准禁止自愿添加constexpr到标准类。

于 2021-11-16T13:49:55.790 回答