在前面两节:《探索C++多线程》:thread源码(一)与《探索C++多线程》:thread源码(二)中,我们学习了多线程的基本内容,那么接下来这一节中将学习到多线程的互斥与多线程锁的相关知识,这包含于头文件<mutex>中。
互斥是为了防止多线程同时访问共享资源而产生的数据竞争,并提供多线程的同步支持。
互斥量mutex的类型:
mutex |
C++11 |
基本的互斥量 |
timed_mutex |
C++11 |
一定时间内尝试锁定的互斥量 |
recursive_mutex |
C++11 | 可由同一线程递归锁定的互斥量 |
recursive_timed_mutex |
C++11 | 可由同一线程递归锁定的timed互斥量 |
shared_mutex |
C++17 | 共享的互斥量 |
shared_timed_mutex |
C++14 | 共享的timed互斥量 |
关于mutex
std::mutex类是C++11中基本的互斥量,其定义如下:
class mutex : public _Mutex_base { // class for mutual exclusion
public:
mutex() _NOEXCEPT : _Mutex_base() { }
mutex(const mutex&) = delete;
mutex& operator=(const mutex&) = delete;
};
可以看出,mutex是基类_Mutex_base的派生类,mutex类中禁用了拷贝构造函数和赋值函数。实际上列出的6种mutex互斥量都是_Mutex_base的派生类。因此我们将目光转向_Mutex_base基类,看看这个基类中到底提供了什么,其定义如下:
class _Mutex_base {
public:
_Mutex_base(int _Flags = 0) { // 构造函数
_Mtx_initX(&_Mtx, _Flags | _Mtx_try);
}
~_Mutex_base() _NOEXCEPT { // 析构函数
_Mtx_destroy(&_Mtx);
}
_Mutex_base(const _Mutex_base&) = delete;
_Mutex_base& operator=(const _Mutex_base&) = delete;
void lock() { // 加锁
_Mtx_lockX(&_Mtx);
}
bool try_lock() { // try to 加锁
return (_Mtx_trylockX(&_Mtx) == _Thrd_success);
}
void unlock() { // 解锁
_Mtx_unlockX(&_Mtx);
}
typedef void *native_handle_type;
native_handle_type native_handle() { // return Concurrency::critical_section * as void *
return (_Mtx_getconcrtcs(&_Mtx));
}
private:
friend class _Timed_mutex_base;
friend class condition_variable;
_Mtx_t _Mtx;
};
由上面的源码可知,_Mutex_base基类提供了三个基本的方法:lock()、try_lock()、unlock()。接下来我们就这三个方法逐个讲解。lock()
锁定线程互斥量(mutex),在必要的时候会阻塞线程:
① 若mutex没有被任何线程锁定,则A线程调用此方法会将mutex锁定(从此时开始直到调用unlock,此期间线程A持有mutex);
② 若mutex被线程A锁定,则线程B调用此方法会被阻塞,直到线程A调用了unlock(不再持有互斥量);
③ 若mutex被同一个线程调用,将会导致死锁(如:线程A调用了lock,在未调用unlock的情况,又调用了lock)。如果有这样的刚需,请使用递归互斥量recursive_mutex。
try_lock()
尝试对互斥量(mutex)加锁,返回true或false,是非阻塞的:
① 若mutex没有被任何线程锁定,则A线程调用此方法会将mutex锁定,并返回ture(从此时开始直到调用unlock,此期间线程A持有mutex);
② 若mutex被线程A锁定,则线程B调用此方法,会返回false;但并不阻塞线程B(线程B继续执行);
③若mutex被同一个线程调用,将会导致死锁(如:线程A调用了try_lock,在未调用unlock的情况,又调用了try_lock)。如果有这样的刚需,请使用递归互斥量recursive_mutex。
unlock()
解锁互斥量,释放所有权。
看一段如何使用try_lock()的代码:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
volatile int counter(0); // non-atomic counter
mutex mtx;
void attempt_10k_increases() {
for (int i = 0; i < 10000; ++i) {
if (mtx.try_lock()) { // only increase if currently not locked:
++counter;
mtx.unlock();
}
}
}
int main() {
thread threads[10];
for (int i = 0; i < 10; ++i) {
threads[i] = thread(attempt_10k_increases);
}
for (auto& th : threads) {
th.join();
}
cout << counter << " successful increases of the counter.\n";
return 0;
}
关于recursive_mutex
这是一个递归互斥量,就像mutex一样,但是允许同一线程在recursive_mutex对象上获取多个级别的所有权。该类定义如下:
class recursive_mutex : public _Mutex_base {
public:
recursive_mutex() : _Mutex_base(_Mtx_recursive) { }
recursive_mutex(const recursive_mutex&) = delete;
recursive_mutex& operator=(const recursive_mutex&) = delete;
};
在此处注意,构造函数中基类构造函数的参数为_Mtx_recursive,这是一个枚举量,用这个量来表示mutex的类型;该枚举定义如下:
enum { /* mutex types */
_Mtx_plain = 0x01,
_Mtx_try = 0x02,
_Mtx_timed = 0x04, // 时间互斥量
_Mtx_recursive = 0x100 // 递归互斥量
};
另外再回顾一下,基类构造函数中默认的参数_Flags为0。由定义可知,recursive_mutex递归互斥量与基本的mutex互斥量所拥有的方法是一样的,这里就不再过多阐述了。
关于timed_mutex
这个是时间互斥量,也就是说与时间有关的。
该类定义如下:
class timed_mutex : public _Timed_mutex_base {
public:
timed_mutex() : _Timed_mutex_base() { }
timed_mutex(const timed_mutex&) = delete;
timed_mutex& operator=(const timed_mutex&) = delete;
};
可以知道,该类是_Timed_mutex_base的派生类。因此我们将目光转向_Timed_mutex_base类,其定义如下:
class _Timed_mutex_base : public _Mutex_base {
public:
_Timed_mutex_base(int _Flags = 0) : _Mutex_base(_Flags | _Mtx_timed) { }
_Timed_mutex_base(const _Timed_mutex_base&) = delete;
_Timed_mutex_base& operator=(const _Timed_mutex_base&) = delete;
// 1
template<class _Rep, class _Period>
bool try_lock_for( const chrono::duration<_Rep, _Period>& _Abs_time) { // 在_Abs_time时间内尝试锁定互斥量
stdext::threads::xtime _Tgt = _To_xtime(_Rel_time);
return (try_lock_until(&_Tgt)); // 调用3
}
// 2
template<class _Clock, class _Duration>
bool try_lock_until( const chrono::time_point<_Clock, _Duration>& _Abs_time) { // 尝试锁定互斥量,直到指定的时间点到来(绝对时间点)
typename chrono::time_point<_Clock, _Duration>::duration _Rel_time = _Abs_time - _Clock::now();
return (try_lock_for(_Rel_time)); // 调用1
}
// 3(落到实处)
bool try_lock_until(const xtime *_Abs_time) { // 尝试锁定互斥量,至多阻塞_Abs_time
return (_Mtx_timedlockX(&_Mtx, _Abs_time) == _Thrd_success);
}
};
由上可知,timed_mutex类中除了lock()、try_lock()、unlock()方法外,还增加了2个方法:try_lock_for()、try_lock_until()。
根据上面代码中的注释,我们就可以了解到新增的两个方法的用途,总的说来:
尝试锁定timed_mutex互斥量,至多会阻塞 t 时间:
①若timed_mutex没有被任何线程锁定,则A线程调用新增方法会将timed_mutex锁定(从此时开始直到调用unlock,此期间线程A持有timed_mutex);
② 若timed_mutex被线程A锁定,则线程B调用新增方法会被阻塞,直到线程A不再持有timed_mutex或时间abs_time到来(以先发生者为准);
[1] 若在 t 时间内线程A不再持有timed_mutex,此时线程B会尝试加锁获取所有权,如加锁成功则返回true,若加锁失败(可能被其他线程如C,D...等争夺到了),则返回false;
[2] 若 t 到了,线程A还持有timed_mutex,此时线程B会尝试加锁获取所有权,当然加锁会失败,返回false 。
③ 若timed_mutex被同一个线程调用,将会导致死锁(如果有这样的刚需,请使用recursive_timed_mutex)。
关于recursive_timed_mutex
其定义如下:
class recursive_timed_mutex : public _Timed_mutex_base {
public:
recursive_timed_mutex() : _Timed_mutex_base(_Mtx_recursive) { }
recursive_timed_mutex(const recursive_timed_mutex&) = delete;
recursive_timed_mutex& operator=(const recursive_timed_mutex&) = delete;
};
这是一个结合了recursive_mutex与timed_mutex特点的互斥量,它允许同一线程在recursive_timed_mutex对象上获取多个级别的所有权和定时尝试锁定请求。
当然这种类型的互斥量拥有以前所有的方法和特性,这里也不再过多阐述。
另外两种互斥量
另外,还有两种互斥量:shared_mutex、shared_timed_mutex,由于笔者IDE是VS2013不支持C++14/17的特性,所以这里暂不做分析,以后升级了IDE之后再来补充。
大概说一下:对于shared_mutex与shared_timed_mutex的访问,分为两个层次:
共享:多个线程可以共享一个 shared_mutex / shared_timed_mutex 的所有权;
独占:只有一个线程可以拥有 shared_mutex / shared_timed_mutex 的所有权。
OK,到此为止就分析了几种mutex的特性及其用途用法,根据需要的场景来选取互斥量类型,以避免并发执行时数据竞争问题导致的出错。
若有理解失误或表达不清的地方,还请大家多多指教,谢谢~