一、读写锁
有过多语言编程的开发人员,往往在别的语言中(如Java等)看到过读写锁,它特别适合于在写少读多的情况下。一般在互联网的应用场景中,这种读非常多,写很少的场景非常普遍,所以读写锁其实应用非常多。
在C++中,原来没有这种所谓读写锁之说,只有轻量级锁和重量级锁。虽然可以通过这些锁来模拟实现读写锁,但毕竟不如原生库中带着好用。STL的发展就是要与时俱进,也要看到实际的应用需求,所以在C++14中推出了std::shared_lock。
std::shared_lock是一个通用的共享互斥体的封装,它允许延迟锁定、定时锁定和锁所有权的转移。其中使用的共享互斥体类似std::unique_lock中使用的排它性互斥体,有对比就好理解。因为它的锁是可共享的,所以它就实现了读锁的处理。写锁自然可以使用排它型锁std::unique_lock 或std::lock_gurad。
二、std::shared_lock
std::shared_lock的原理其实比较简单,先看一下其定义:
template<typename _Mutex>class shared_lock{public:typedef _Mutex mutex_type;// Shared lockingshared_lock() noexcept : _M_pm(nullptr), _M_owns(false) { }explicitshared_lock(mutex_type& __m): _M_pm(std::__addressof(__m)), _M_owns(true){ __m.lock_shared(); }shared_lock(mutex_type& __m, defer_lock_t) noexcept: _M_pm(std::__addressof(__m)), _M_owns(false) { }shared_lock(mutex_type& __m, try_to_lock_t): _M_pm(std::__addressof(__m)), _M_owns(__m.try_lock_shared()) { }shared_lock(mutex_type& __m, adopt_lock_t): _M_pm(std::__addressof(__m)), _M_owns(true) { }template<typename _Clock, typename _Duration>shared_lock(mutex_type& __m,const chrono::time_point<_Clock, _Duration>& __abs_time): _M_pm(std::__addressof(__m)),_M_owns(__m.try_lock_shared_until(__abs_time)) { }template<typename _Rep, typename _Period>shared_lock(mutex_type& __m,const chrono::duration<_Rep, _Period>& __rel_time): _M_pm(std::__addressof(__m)),_M_owns(__m.try_lock_shared_for(__rel_time)) { }~shared_lock(){if (_M_owns)_M_pm->unlock_shared();}shared_lock(shared_lock const&) = delete;shared_lock& operator=(shared_lock const&) = delete;shared_lock(shared_lock&& __sl) noexcept : shared_lock(){ swap(__sl); }shared_lock&operator=(shared_lock&& __sl) noexcept{shared_lock(std::move(__sl)).swap(*this);return *this;}voidlock(){_M_lockable();_M_pm->lock_shared();_M_owns = true;}booltry_lock(){_M_lockable();return _M_owns = _M_pm->try_lock_shared();}template<typename _Rep, typename _Period>booltry_lock_for(const chrono::duration<_Rep, _Period>& __rel_time){_M_lockable();return _M_owns = _M_pm->try_lock_shared_for(__rel_time);}template<typename _Clock, typename _Duration>booltry_lock_until(const chrono::time_point<_Clock, _Duration>& __abs_time){_M_lockable();return _M_owns = _M_pm->try_lock_shared_until(__abs_time);}voidunlock(){if (!_M_owns)__throw_system_error(int(errc::resource_deadlock_would_occur));_M_pm->unlock_shared();_M_owns = false;}// Settersvoidswap(shared_lock& __u) noexcept{std::swap(_M_pm, __u._M_pm);std::swap(_M_owns, __u._M_owns);}mutex_type*release() noexcept{_M_owns = false;return std::__exchange(_M_pm, nullptr);}// Gettersbool owns_lock() const noexcept { return _M_owns; }explicit operator bool() const noexcept { return _M_owns; }mutex_type* mutex() const noexcept { return _M_pm; }private:void_M_lockable() const{if (_M_pm == nullptr)__throw_system_error(int(errc::operation_not_permitted));if (_M_owns)__throw_system_error(int(errc::resource_deadlock_would_occur));}mutex_type* _M_pm;bool _M_owns;};
std::shared_lock有两个明显的特点,也就是和读写锁对应。一个是读的共享性;另外一个是定的独占性。看它的源码中的实现,其实也是猜测的那样,可以使用各种支持共享的互斥体和相关时间状态以及各种对锁的处理策略。没有什么让人一看就迷糊的代码,相对其它的模板类,算是很友好了。
三、例程
看一个例程:
#include <iostream>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <thread>std::string file = "Original content."; // Simulates a file
std::mutex output_mutex; // mutex that protects output operations.
std::shared_mutex file_mutex; // reader/writer mutexvoid read_content(int id)
{std::string content;{std::shared_lock lock(file_mutex, std::defer_lock); // Do not lock it first.lock.lock(); // Lock it here.content = file;}std::lock_guard lock(output_mutex);std::cout << "Contents read by reader #" << id << ": " << content << '\n';
}void write_content()
{{std::lock_guard file_lock(file_mutex);file = "New content";}std::lock_guard output_lock(output_mutex);std::cout << "New content saved.\n";
}int main()
{std::cout << "Two readers reading from file.\n"<< "A writer competes with them.\n";std::thread reader1{read_content, 1};std::thread reader2{read_content, 2};std::thread writer{write_content};reader1.join();reader2.join();writer.join();std::cout << "The first few operations to file are done.\n";reader1 = std::thread{read_content, 3};reader1.join();
}
其运行的结果可能是:
Two readers reading from file.
A writer competes with them.
Contents read by reader #1: Original content.
Contents read by reader #2: Original content.
New content saved.
The first few operations to file are done.
Contents read by reader #3: New content
在实现类似上面的代码中还可以使用std::shared_timed_mutex等相关的互斥体,更有利于精细化的锁的粒度控制。上面的代码有一点点小意外,std::shared_mutex是在C++17才推出的,如果在C++14想使用std::shared_lock,就得使用std::shared_timed_mutex了。同时,大家需要注意输出的锁,仍然是一个独占性锁。
另外,读写同时操作时,有可能出现读写不一致的情况,所以可以应用在不太在意修改的流程中,比如定时刷新显示当前的客户记录,虽然可能修改后的数据未能在第一时间显示到界面上,但其实再刷新一次就会好。
当然,如果需要强制性的一致,那么还是使用排它锁比较好。
四、总结
语言是实现设计的手段和工具,不要太在意它们的不同。选择更合适的实现和达到最终的结果,这才是一个优秀的开发人员需要做到的。而语言中的整体的设计和开发方向其实受计算机的底层限制很大,再加上几乎所有的语言设计者都是精通计算机系统的,其处理问题的思想本质也是相通的。所以,一时的不同,不代表未来的不同;实现的不同,不代表应用的表现不同。
与诸君共勉!