《探索C++多线程》:mutex源码(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hujingshuang/article/details/70237211

        在前面两节:《探索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互斥量,至多会阻塞 时间:
                若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的特性及其用途用法,根据需要的场景来选取互斥量类型,以避免并发执行时数据竞争问题导致的出错。

        若有理解失误或表达不清的地方,还请大家多多指教,谢谢~

猜你喜欢

转载自blog.csdn.net/hujingshuang/article/details/70237211