资源竞争是并发编程不可回避的问题。对于资源竞争,要么设计无锁模式,要么老老实实的加锁排队等待。在Windows和Linux下有很多相关的系统API或对象进行线程同步,比如:
- 互斥体
- 信号量
- 条件变量
- 事件
- 读写锁
- 自旋锁
- future:
但是,在C++11中只提供了互斥体和条件变量来进行线程同步,这里对互斥体进行简单介绍,条件变量另外介绍。同时,如果要实现信号量、读写锁、事件,可以很方便的使用互斥体和条件变量来实现,将在后续的《读写锁、自旋锁、信号量的CPP11实现》中讨论。
互斥体基本用法
互斥体,顾名思义,就是相互排斥的对象,也就是说当一个线程在访问(获取)某一对象的时候,其他线程需要等到其他线程访问完毕之后才能继续访问,也就是说同一时间只能有一个线程对该对象具有访问的权利。我们可以把互斥体想象为一把打开锁的钥匙,只有获得了钥匙才能打开锁拿到资源,当不再访问资源的时候就要把钥匙归还回去,让其他需要访问资源的线程能够获取这把钥匙继续访问资源。 C++11提供的std::mutex类就是这把钥匙,而它的成员函数lock和unlock则对应了加锁和解锁的动作。std::mutex就是那唯一的钥匙,所有需要访问资源的线程都需要先去获取锁(调用lock函数),当不再需要访问资源的时候就需要归还钥匙(调用unlock释放锁)。在一个线程拥有锁的情况下,其他线程都将阻塞在lock函数的调用上,因此为了并发效率的考虑,加锁的粒度要尽量小(也就是拿到货之后要尽快的释放锁)。
下面的代码很简单,创建一个线程池,调用add_job添加job,调用do_some_thing_on_job完成job,所有的job都存放在m_res_queue资源队列中,每个job都是独立的需要单独的线程去完成。因此对于queue的访问需要加锁——添加job的时候,从queue中获取job的时候都需要加锁。而加锁的粒度也应该尽量小,耗时的任务不能放在加锁中。
class work_job
{
public:
work_job(){}
~work_job(){
for(auto t& m_pool)
{
if(t.joinable())
{
t.join();
}
}
}
void do_some_thing_on_job(job_res &res){}
void thread_task()
{
// 获取锁
m_nutex.lock();
job_res res = m_res_queue.front(); // 获取资源
m_res_queue.pop();
// 释放锁
m_mutex.unlock();
do_some_thing_on_job(res);
}
void create_thread()
{
int nThreadCount = 4;
// 工作线程
for(int i = 0; i < nThreadCount;++i)
{
m_pool.emplace_back(std::thread(thread_task));
}
}
void add_job(job_res res)
{
// 获取锁
m_nutex.lock();
m_res_queue.push(res);
// 释放锁
m_mutex.unlock();
}
std::vector<std::thread> m_pool;
std::queue<job_res> m_res_queue;
std::mutex m_nutex;
};
互斥体的辅助类
一般实际工作中很少直接使用std::mutex调用lock和unlock,而是使用std::mutex的辅助类——std::lock_guard和std::unique_lock。
std::lock_guard
std::lock_guard没有太多要说的,它的主要功能是在构造的时候获取锁,在析构的时候释放锁。 注意: std::lock_guard 没有加锁、解锁的接口。
其实现大致如下:
template <typename _MutexType>
class lock_guard
{
public:
explicit lock_guard(std::mutex &m):m_mutex(m)
{
m_mutex.lock();
}
~lock_guard(){
m_mutex.unlock();
}
// 禁止拷贝构造和拷贝赋值,也就是禁止lock_guard所守护的所在对象之间进行转移
lock_guard(const lock_guard &) = delete;
lock_guard & operator=(const lock_guard &) = delete;
private:
_MutexType &m_mutex;
};
std::unique_lock
unique_lock和lock_guard的不同之处在于它不仅在构造的时候加锁在析构的时候释放锁,并且他可以在任意的地方调用lock和unlock,相对于lock_guard有更大的灵活性。 注意:
- 看unique_lock的名字,就知道它是一个具有所有权概念的类。和unique_ptr一样,一旦一个unique_lock被赋值给另一个unique_lock(新对象),那么新的对象将释放锁的所有权,然后新对象将获得锁的所有权然后处于和原来的锁一样的状态,这一点可以看一下unique_lock的拷贝赋值函数的实现,很简单。
- unique_lock如果已经加锁,然后再次尝试去加锁,将抛出异常!这一点可以参考其_Validate函数的实现。
以下为该类的实现:
template <class _Mutex>
class unique_lock { // whizzy class with destructor that unlocks mutex
public:
using mutex_type = _Mutex;
// CONSTRUCT, ASSIGN, AND DESTROY
unique_lock() noexcept : _Pmtx(nullptr), _Owns(false) {}
explicit unique_lock(_Mutex& _Mtx) : _Pmtx(_STD addressof(_Mtx)), _Owns(false) { // construct and lock
_Pmtx->lock();
_Owns = true;
}
unique_lock(_Mutex& _Mtx, adopt_lock_t)
: _Pmtx(_STD addressof(_Mtx)), _Owns(true) {} // construct and assume already locked
unique_lock(_Mutex& _Mtx, defer_lock_t) noexcept
: _Pmtx(_STD addressof(_Mtx)), _Owns(false) {} // construct but don't lock
unique_lock(_Mutex& _Mtx, try_to_lock_t)
: _Pmtx(_STD addressof(_Mtx)), _Owns(_Pmtx->try_lock()) {} // construct and try to lock
template <class _Rep, class _Period>
unique_lock(_Mutex& _Mtx, const chrono::duration<_Rep, _Period>& _Rel_time)
: _Pmtx(_STD addressof(_Mtx)), _Owns(_Pmtx->try_lock_for(_Rel_time)) {} // construct and lock with timeout
template <class _Clock, class _Duration>
unique_lock(_Mutex& _Mtx, const chrono::time_point<_Clock, _Duration>& _Abs_time)
: _Pmtx(_STD addressof(_Mtx)), _Owns(_Pmtx->try_lock_until(_Abs_time)) {} // construct and lock with timeout
unique_lock(_Mutex& _Mtx, const xtime* _Abs_time)
: _Pmtx(_STD addressof(_Mtx)), _Owns(false) { // try to lock until _Abs_time
_Owns = _Pmtx->try_lock_until(_Abs_time);
}
unique_lock(unique_lock&& _Other) noexcept : _Pmtx(_Other._Pmtx), _Owns(_Other._Owns) {
_Other._Pmtx = nullptr;
_Other._Owns = false;
}
unique_lock& operator=(unique_lock&& _Other) {
if (this != _STD addressof(_Other)) {
if (_Owns) {
_Pmtx->unlock();
}
_Pmtx = _Other._Pmtx;
_Owns = _Other._Owns;
_Other._Pmtx = nullptr;
_Other._Owns = false;
}
return *this;
}
~unique_lock() noexcept {
if (_Owns) {
_Pmtx->unlock();
}
}
unique_lock(const unique_lock&) = delete;
unique_lock& operator=(const unique_lock&) = delete;
void lock() { // lock the mutex
_Validate();
_Pmtx->lock();
_Owns = true;
}
_NODISCARD bool try_lock() {
_Validate();
_Owns = _Pmtx->try_lock();
return _Owns;
}
template <class _Rep, class _Period>
_NODISCARD bool try_lock_for(const chrono::duration<_Rep, _Period>& _Rel_time) {
_Validate();
_Owns = _Pmtx->try_lock_for(_Rel_time);
return _Owns;
}
template <class _Clock, class _Duration>
_NODISCARD bool try_lock_until(const chrono::time_point<_Clock, _Duration>& _Abs_time) {
_Validate();
_Owns = _Pmtx->try_lock_until(_Abs_time);
return _Owns;
}
_NODISCARD bool try_lock_until(const xtime* _Abs_time) {
_Validate();
_Owns = _Pmtx->try_lock_until(_Abs_time);
return _Owns;
}
void unlock() {
if (!_Pmtx || !_Owns) {
_Throw_system_error(errc::operation_not_permitted);
}
_Pmtx->unlock();
_Owns = false;
}
void swap(unique_lock& _Other) noexcept {
_STD swap(_Pmtx, _Other._Pmtx);
_STD swap(_Owns, _Other._Owns);
}
_Mutex* release() noexcept {
_Mutex* _Res = _Pmtx;
_Pmtx = nullptr;
_Owns = false;
return _Res;
}
_NODISCARD bool owns_lock() const noexcept {
return _Owns;
}
explicit operator bool() const noexcept {
return _Owns;
}
_NODISCARD _Mutex* mutex() const noexcept {
return _Pmtx;
}
private:
_Mutex* _Pmtx;
bool _Owns;
void _Validate() const { // check if the mutex can be locked
if (!_Pmtx) {
_Throw_system_error(errc::operation_not_permitted);
}
if (_Owns) {
_Throw_system_error(errc::resource_deadlock_would_occur);
}
}
};
可以看到,unique_lock比lock_guard的更大灵活性不仅体现在有lock和unlock函数上,它还提供了更具灵活性的构造函数(在构造的时候决定是否加锁,构造的时候尝试加锁及尝试加锁的超时时间等)。你还可以在任意时刻使用release释放锁——该函数返回其所拥有的锁(假如有)并将自己的状态重置(并不改变原来锁的状态)。
std::mutex m;
auto ulock_1 = std::unique_lock(m); // 初始化并加锁
auto ulock_2(ulock_1); // m所有权从ulock_1转移到ulock_2
time_mutex
class timed_mutex { // class for timed mutual exclusion
public:
timed_mutex() noexcept : _My_locked(0) {}
timed_mutex(const timed_mutex&) = delete;
timed_mutex& operator=(const timed_mutex&) = delete;
void lock() { // lock the mutex
unique_lock<mutex> _Lock(_My_mutex);
while (_My_locked != 0) {
_My_cond.wait(_Lock);
}
_My_locked = UINT_MAX;
}
_NODISCARD bool try_lock() noexcept { // try to lock the mutex
lock_guard<mutex> _Lock(_My_mutex);
if (_My_locked != 0) {
return false;
} else {
_My_locked = UINT_MAX;
return true;
}
}
void unlock() { // unlock the mutex
{
// The lock here is necessary
lock_guard<mutex> _Lock(_My_mutex);
_My_locked = 0;
}
_My_cond.notify_one();
}
template <class _Rep, class _Period>
_NODISCARD bool try_lock_for(const chrono::duration<_Rep, _Period>& _Rel_time) { // try to lock for duration
return try_lock_until(chrono::steady_clock::now() + _Rel_time);
}
template <class _Time>
bool _Try_lock_until(_Time _Abs_time) { // try to lock the mutex with timeout
unique_lock<mutex> _Lock(_My_mutex);
if (!_My_cond.wait_until(_Lock, _Abs_time, _UInt_is_zero{_My_locked})) {
return false;
}
_My_locked = UINT_MAX;
return true;
}
template <class _Clock, class _Duration>
_NODISCARD bool try_lock_until(
const chrono::time_point<_Clock, _Duration>& _Abs_time) { // try to lock the mutex with timeout
return _Try_lock_until(_Abs_time);
}
_NODISCARD bool try_lock_until(const xtime* _Abs_time) { // try to lock the mutex with timeout
return _Try_lock_until(_Abs_time);
}
private:
mutex _My_mutex;
condition_variable _My_cond;
unsigned int _My_locked;
};
嵌套锁——recursive_mutex
C++标准库提供了std::recursive_mutex类,其功能与std::mutex类似,不同的是你可以在同一线程的单个实例上多次进行加锁操作。一个线程在获得嵌套锁之前,其他线程需要先释放掉该嵌套锁的所有权,一旦获得所有权那么你可以多次进行加锁操作,但是要释放锁那么你加锁几次就应该解锁一次。
大多数情况下,如果你要使用嵌套锁,那么你就应该考虑是否需要重新设计你的程序,因为嵌套锁带来的好处远远小于其所带来的坏处,此处不表。
文档信息
- 本文作者:will kall
- 本文链接:https://fishlovee.github.io/2020/07/03/02-%E4%BA%92%E6%96%A5%E4%BD%93/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)