std::scoped_lock 的源码实现与解析
本单章专门介绍标准库在 C++17 引入的类模板 std::scoped_lock 的实现,让你对它再无疑问。
这会涉及到不少的模板技术,这没办法,就如同我们先前聊 std::thread 的构造与源码分析最后说的:“不会模板,你阅读标准库源码,是无稽之谈”。建议学习现代C++模板教程。
我们还是一样的,以 MSVC STL 实现的 std::scoped_lock 代码进行讲解,不用担心,我们也查看了 libstdc++ 、libc++的实现,并没有太多区别,更多的是一些风格上的。而且个人觉得 MSVC 的实现是最简单直观的。
std::scoped_lock 的数据成员
std::scoped_lock 的数据成员std::scoped_lock 是一个类模板,它有两个特化,也就是有三个版本,其中的数据成员也是不同的。并且它们都不可移动不可复制,“管理类”应该如此。
主模板,是一个可变参数类模板,声明了一个类型形参包
_Mutexes,存储了一个std::tuple,具体类型根据类型形参包决定。_EXPORT_STD template <class... _Mutexes> class _NODISCARD_LOCK scoped_lock { // class with destructor that unlocks mutexes public: explicit scoped_lock(_Mutexes&... _Mtxes) : _MyMutexes(_Mtxes...) { // construct and lock _STD lock(_Mtxes...); } explicit scoped_lock(adopt_lock_t, _Mutexes&... _Mtxes) noexcept // strengthened : _MyMutexes(_Mtxes...) {} // construct but don't lock ~scoped_lock() noexcept { _STD apply([](_Mutexes&... _Mtxes) { (..., (void) _Mtxes.unlock()); }, _MyMutexes); } scoped_lock(const scoped_lock&) = delete; scoped_lock& operator=(const scoped_lock&) = delete; private: tuple<_Mutexes&...> _MyMutexes; };对模板类型形参包只有一个类型情况的偏特化,是不是很熟悉,和
lock_guard几乎没有任何区别,保有一个互斥量的引用,构造上锁,析构解锁,提供一个额外的构造函数让构造的时候不上锁。所以用scoped_lock替代lock_guard不会造成任何额外开销。template <class _Mutex> class _NODISCARD_LOCK scoped_lock<_Mutex> { public: using mutex_type = _Mutex; explicit scoped_lock(_Mutex& _Mtx) : _MyMutex(_Mtx) { // construct and lock _MyMutex.lock(); } explicit scoped_lock(adopt_lock_t, _Mutex& _Mtx) noexcept // strengthened : _MyMutex(_Mtx) {} // construct but don't lock ~scoped_lock() noexcept { _MyMutex.unlock(); } scoped_lock(const scoped_lock&) = delete; scoped_lock& operator=(const scoped_lock&) = delete; private: _Mutex& _MyMutex; };对类型形参包为空的情况的全特化,没有数据成员。
template <> class scoped_lock<> { public: explicit scoped_lock() = default; explicit scoped_lock(adopt_lock_t) noexcept /* strengthened */ {} scoped_lock(const scoped_lock&) = delete; scoped_lock& operator=(const scoped_lock&) = delete; };
std::scoped_lock的构造与析构
std::scoped_lock的构造与析构在上一节讲 scoped_lock 的数据成员的时候已经把这个模板类的全部源码,三个版本的代码都展示了,就不再重复。
这三个版本中,只有两个版本需要介绍,也就是
形参包元素数量为一的偏特化,只管理一个互斥量的。
主模板,可以管理任意个数的互斥量。
那这两个的共同点是什么呢?构造上锁,析构解锁。这很明显,明确这一点我们就开始讲吧。
这段代码为你展示了 std::lock_guard 和 std::scoped_lock 形参包元素数量为一的偏特化的唯一区别:调用不会上锁的构造函数的参数顺序不同。那么到此也就够了。
接下来我们进入 std::scoped_lock 主模板的讲解:
这个构造函数做了两件事情,初始化数据成员 _MyMutexes让它保有这些互斥量的引用,以及给所有互斥量上锁,使用了 std::lock 帮助我们完成这件事情。
这个构造函数不上锁,只是初始化数据成员 _MyMutexes让它保有这些互斥量的引用。
析构函数就要稍微聊一下了,主要是用 std::apply 去遍历 std::tuple ,让元组保有的互斥量引用都进行解锁。简单来说是 std::apply 可以将元组存储的参数全部拿出,用于调用这个可变参数的可调用对象,我们就能利用折叠表达式展开形参包并对其调用 unlock()。
不在乎其返回类型只用来实施它的副作用,显式转换为
(void)也就是弃值表达式。在我们之前讲的std::thread源码中也有这种用法。不过你可能有疑问:“我们的标准库的那些互斥量
unlock()返回类型都是void呀,为什么要这样?”的确,这是个好问题,libstdc++ 和 libc++ 都没这样做,或许 MSVC STL 想着会有人设计的互斥量让它的
unlock()返回类型不为void,毕竟 互斥体 (Mutex) 没有要求unlock()的返回类型。
这个函数模板接受两个参数,一个可调用 (Callable)对象 f,以及一个元组 t,用做调用 f 。我们可以自己简单实现一下它,其实不算难,这种遍历元组的方式在之前讲 std::thread 的源码的时候也提到过。
其实就是把元组给解包了,利用了 std::index_sequence + std::make_index_sequence 然后就用 std::get 形参包展开用 std::invoke 调用可调用对象即可,非常经典的处理可变参数做法,这个非常重要,一定要会使用。
举一个简单的调用例子:
运行测试。
使用了折叠表达式展开形参包,打印了元组所有的元素。
总结
如你所见,其实这很简单。至少使用与了解其设计原理是很简单的。唯一的难度或许只有那点源码,处理可变参数,这会涉及不少模板技术,既常见也通用。还是那句话:“不会模板,你阅读标准库源码,是无稽之谈”。
相对于 std::thread 的源码解析,std::scoped_lock 还是简单的多。
Last updated