mutex类是可以用来保护共享数据的同步原语,防止被其他线程修改,也就是保证了该线程对共享数据的独占式访问。
简而言之,互斥是为了防止多线程同时访问共享资源而产生数据竞争,并提供多线程的同步支持。
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类中禁用了拷贝构造函数和赋值函数,因此我们将目光转向_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(),unclock()。接下来我们就这三个方法逐个讲解:
锁定线程互斥量(mutex),在必要的时候会阻塞线程:
① 若mutex没有被任何线程锁定,则A线程调用此方法会将mutex锁定(从此时开始直到调用unlock,此期间线程A持有mutex);
② 若mutex被线程A锁定,则线程B调用此方法会被阻塞,直到线程A调用了unlock(不再持有互斥量);
③ 若mutex被同一个线程调用,将会导致死锁(如:线程A调用了lock,在未调用unlock的情况,又调用了lock)。如果有这样的刚需,请使用递归互斥量recursive_mutex。
示例代码:
#include
#include
#include
#include
int g_num = 0; // protected by g_num_mutex
std::mutex g_num_mutex;
void slow_increment(int id)
{
for (int i = 0; i < 3; ++i) {
g_num_mutex.lock();
++g_num;
std::cout << id << " => " << g_num << '\n';
g_num_mutex.unlock();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
int main()
{
std::thread t1(slow_increment, 0);
std::thread t2(slow_increment, 1);
t1.join();
t2.join();
}
//////output
0 => 1
1 => 2
0 => 3
1 => 4
0 => 5
1 => 6
尝试对互斥量进行加锁,返回true或者是false,是非阻塞的。
① 若mutex没有被任何线程锁定,则A线程调用此方法会将mutex锁定,并返回ture(从此时开始直到调用unlock,此期间线程A持有mutex);
② 若mutex被线程A锁定,则线程B调用此方法,会返回false;但并不阻塞线程B(线程B继续执行);
③若mutex被同一个线程调用,将会导致死锁(如:线程A调用了try_lock,在未调用unlock的情况,又调用了try_lock)。如果有这样的刚需,请使用递归互斥量recursive_mutex。
示例代码:
#include
#include
#include
#include // std::cout
std::chrono::milliseconds interval(100);
std::mutex mutex;
int job_shared = 0; // both threads can modify 'job_shared',
// mutex will protect this variable
int job_exclusive = 0; // only one thread can modify 'job_exclusive'
// no protection needed
// this thread can modify both 'job_shared' and 'job_exclusive'
void job_1()
{
std::this_thread::sleep_for(interval); // let 'job_2' take a lock
while (true) {
// try to lock mutex to modify 'job_shared'
if (mutex.try_lock()) {
std::cout << "job shared (" << job_shared << ")\n";
mutex.unlock();
return;
} else {
// can't get lock to modify 'job_shared'
// but there is some other work to do
++job_exclusive;
std::cout << "job exclusive (" << job_exclusive << ")\n";
std::this_thread::sleep_for(interval);
}
}
}
// this thread can modify only 'job_shared'
void job_2()
{
mutex.lock();
std::this_thread::sleep_for(5 * interval);
++job_shared;
mutex.unlock();
}
int main()
{
std::thread thread_1(job_1);
std::thread thread_2(job_2);
thread_1.join();
thread_2.join();
}
////output
job exclusive (1)
job exclusive (2)
job exclusive (3)
job exclusive (4)
job shared (1)
解锁互斥量,释放所有权。
lock_guard | C++11 | 区域锁 |
unique_lock | C++11 | 区域锁,提供了更加灵活的操作 |
shared_lock | C++14 | 提供共享的互斥量的锁操作 |
scoped_lock | C++17 | 提供多个互斥量时避免死锁RAII的锁操作 |
很多时候我们在编写多线程程序的时候,在对互斥量加锁后,有时候可能会忘记解锁unclock(),这样就会导致该线程一直持有互斥量,而其他与其存在明显资源竞争的线程会一直获取不到所有权,这显然违背了我们多线程处理任务的愿望。
在了解lock_guard之前,让我们先看一段代码:
#include
#include
#include
using namespace std;
volatile int cnt = 0;
mutex mtx;
void task() {
for (int i = 0; i < 10; i++) {
lock_guard lck(mtx); // 使用lock_guard
cout << this_thread::get_id() << "-----" << cnt++ << endl;
}
}
int main() {
thread t[3];
for (int i = 0; i < 3; i++) {
t[i] = thread(task);
}
for (int i = 0; i < 3; i++) {
t[i].join();
}
return 0;
}
看完上述代码,大家可能会有疑问,虽然有互斥量mtx,但是并没有调用mtx对象的方法,mtx.lock()或者是mtx.try_lock(),也没有出现mtx.unclock()啊,那么程序是怎么做到加锁与解锁的呢?
这就是lock_guard
定义如下:
template
class lock_guard { // 利用析构函数解锁互斥量
public:
typedef _Mutex mutex_type;
explicit lock_guard(_Mutex& _Mtx) : _MyMutex(_Mtx) { // 构造,并加锁
_MyMutex.lock();
}
lock_guard(_Mutex& _Mtx, adopt_lock_t) : _MyMutex(_Mtx) { } // 构造,但不加锁
~lock_guard() _NOEXCEPT { // 析构函数,解锁
_MyMutex.unlock();
}
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
_Mutex& _MyMutex;
};
由上述定义可知,这是一个类模板,结构很简单,该类中定义了一个引用变量,还提供了构造函数,析构函数,禁用了拷贝构造函数以及赋值函数。
lock_guard
我们再看看上述代码都做了什么事情,显然他使用了构造函数,在初始化列表中初始化了互斥量,在函数体中对互斥量进行了上锁,因此实例化一个对象的时候,就已经上锁了。
那么什么时候解锁的呢?我们看到,在调用析构函数的时候,对互斥量进行了解锁。在上面的代码中,在for循环中,每一次结束循环,便会自动将lck析构掉,因此lock_guard的原理便简单明了了。
总的来说,lock_guard的本质就是,利用程序块(block)结束,对象被析构时候自动解锁。因此,我们只需要将进行加锁的操作(设计资源共享的代码部分)与实例化lock_guard对象放在一个语句块中即可,如下:
volatile int cnt = 0;
mutex mtx;
void task() {
for (int i = 0; i < 1000; i++) {
{ // begin
lock_guard lck(mtx);
cnt++;
if (cnt > 1000) cnt = 1000;
else if (cnt < 0) cnt = 0;
} // end
}
}
另外,lock_guard类中还有一个方法,在源码的注释中讲到了,该方法也是实例化一个对象,但不对互斥量上锁。主要是为了避免互斥量在已经被上锁的情况下再上锁,用法如下:
volatile int cnt = 0;
mutex mtx;
void task() {
for (int i = 0; i < 1000; i++) {
{ // begin
mtx.lock(); // 已上锁
lock_guard lck(mtx, adopt_lock);
cnt++;
if (cnt > 1000) cnt = 1000;
else if (cnt < 0) cnt = 0;
} // end
}
}
其中adopt_lock是一个空类(结构体),相信读过侯JJ《STL源码剖析》的都知道(用一个空类或结构体来进行方法的重载的用法),这里也是同样的用法。
其声明与定义如下:
struct adopt_lock_t { };
struct defer_lock_t { };
struct try_to_lock_t { };
extern _CRTIMP2_PURE const adopt_lock_t adopt_lock;
extern _CRTIMP2_PURE const defer_lock_t defer_lock;
extern _CRTIMP2_PURE const try_to_lock_t try_to_lock;
好了,以上就是对lock_guard的解析、用途和使用用法。
参考博文:
https://blog.csdn.net/hujingshuang/article/details/70243025
https://en.cppreference.com/w/cpp/thread/lock_guard