基于C++11并发库的线程池与消息队列多线程框架——std::mutex类

mutex类是可以用来保护共享数据的同步原语,防止被其他线程修改,也就是保证了该线程对共享数据的独占式访问。

简而言之,互斥是为了防止多线程同时访问共享资源而产生数据竞争,并提供多线程的同步支持。

mutex  提供了独占式的,非递归的所有权语义:

  1. 线程拥有互斥量的周期:从调用lock()或者是try_lock()开始,到调用unclock()为止;
  2. 当线程拥有互斥量后,所有其他的线程尝试拥有该互斥量后都会处于阻塞状态;
  3. 线程在调用lock()或者是try_lock()之前不能够拥有互斥量

 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()。接下来我们就这三个方法逐个讲解:

1  lock()

锁定线程互斥量(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

2 try_lock()

 尝试对互斥量进行加锁,返回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)

3 unclock()

    解锁互斥量,释放所有权。

4 锁lock()的类型

lock_guard C++11 区域锁
unique_lock C++11 区域锁,提供了更加灵活的操作
shared_lock C++14 提供共享的互斥量的锁操作
scoped_lock C++17 提供多个互斥量时避免死锁RAII的锁操作

  很多时候我们在编写多线程程序的时候,在对互斥量加锁后,有时候可能会忘记解锁unclock(),这样就会导致该线程一直持有互斥量,而其他与其存在明显资源竞争的线程会一直获取不到所有权,这显然违背了我们多线程处理任务的愿望。

关于lock_guard

在了解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 lck(mtx) 这句代码起到的功效了。好的,现在我们带着疑问来解读一下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 lck(mtx)

我们再看看上述代码都做了什么事情,显然他使用了构造函数,在初始化列表中初始化了互斥量,在函数体中对互斥量进行了上锁,因此实例化一个对象的时候,就已经上锁了。

那么什么时候解锁的呢?我们看到,在调用析构函数的时候,对互斥量进行了解锁。在上面的代码中,在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

你可能感兴趣的:(c++)