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

C++多线程支持库(Thread support library)

        C++的内置支持包括thread(线程),mutual exclusion(互斥),condition variables(条件变量)和future等。

        头文件有:

                <thread> 

                <mutex> 

                <condition_variable> 

                <future>

        先来说头文件<thread>,这里面有两个东西,一个是thread类(class thread),一个是this_thread命名空间(namespace this_thread,该命名空间中有几个有用的函数),另外类thread中还有个内部id(class id)。

多线程举例

        通过一段多线程的代码,我们一步步来揭开thread的面纱,代码如下:

#include <iostream>
#include <thread>

using namespace std;

void task_one() {
    for (int i = 0; i < 10; i++) {
        cout << this_thread::get_id() << '\t' << i << endl;
        this_thread::sleep_for(chrono::milliseconds(5));    // 休眠5ms
    }
}

void task_two(int n) {
    for (int i = 0; i < n; i++) {
        cout << this_thread::get_id() << '\t' << i << endl;
        this_thread::sleep_for(chrono::milliseconds(10));   //休眠10ms
    }
}

int main() {
    int n = 20;

    thread t1(task_one);
    thread t2(task_two, n);

    t1.join();
    t2.join();

    return 0;
}
        上述代码中,一共存在三个线程,t1,t2和程序主线程(也就是执行main的那个线程)。线程t1、t2的任务分别是执行task_one、task_two(也就是两个函数),在各自的线程中打印线程id及循环量i。

        另外代码t1.join()和t2.join()在main函数中,也就是说在主线程中,表示将主线程与线程t1、t2相结合。这样一来,主线程会阻塞,直到线程t1、t2执行完毕,主线程才会执行后面的代码。

线程的join与detach

        在这里要说明线程的两种状态:在任何一个时刻,线程是结合分离 状态:

                1、一个结合状态的线程能够被其他线程回收资源和杀死,在被其他线程回收之前,它所占有的资源是不释放的;

                2、一个分离状态的线程是不能被其他线程回收或杀死的,它所占有的资源会在该线程执行完毕后由系统自动释放

        线程的结合和分离状态决定了一个线程以什么样的方式来终止自己,在默认情况下线程是非分离的状态。

        OK,大家有了上述的概念,我们就可以开始看<thread>源码了,代码也不是很长,大家预览一遍有个印象就可以了,分析在后面。(VS2013源码在这里:\Microsoft Visual Studio 12.0\VC\include\thread)

// 管理线程的类
class thread {
public:
	class id;                   // 内部类,后面会分析
	typedef void *native_handle_type;

	thread() _NOEXCEPT {	    // 构造函数,空线程
		_Thr_set_null(_Thr);    // 宏定义,原型为:#define _Thr_set_null(thr) (thr._Id = 0)
	}

	template<class _Fn, class... _Args>
    explicit thread(_Fn&& _Fx, _Args&&... _Ax) {	// 带参模板构造函数_Fx(_Ax...)
		_Launch(&_Thr, _STD bind(_Decay_copy(_STD forward<_Fn>(_Fx)), _Decay_copy(_STD forward<_Args>(_Ax))...));
    }

	~thread() _NOEXCEPT {	    // 析构函数
		if (joinable())         // 线程是可结合的,析构异常(也就是说只能析构不可结合的线程)
			_XSTD terminate();  // terminate会调用abort()来终止程序
	}

	thread(thread&& _Other) _NOEXCEPT : _Thr(_Other._Thr) {	    // 拷贝构造函数,调用move
		_Thr_set_null(_Other._Thr);
	}

	thread& operator=(thread&& _Other) _NOEXCEPT {	// 赋值函数,调用move
		return (_Move_thread(_Other));
	}

	thread(const thread&) = delete;                 // 禁用 拷贝构造函数
	thread& operator=(const thread&) = delete;      // 禁用 赋值函数

	void swap(thread& _Other) _NOEXCEPT {	        // 交换两线程
		_STD swap(_Thr, _Other._Thr);
	}

	bool joinable() const _NOEXCEPT {	            // 若线程可结合程,返回 true;否则,返回flase
		return (!_Thr_is_null(_Thr));               // 宏定义,原型为:#define _Thr_is_null(thr) (thr._Id == 0)
	}

	void join();                                    // 线程结合,阻塞的

	void detach() {	                                // 线程分离
		if (!joinable())                            // 若线程是不可结合的,则异常
			_Throw_Cpp_error(_INVALID_ARGUMENT);
		_Thrd_detachX(_Thr);
		_Thr_set_null(_Thr);
	}

	id get_id() const _NOEXCEPT;                    // 获取线程唯一 id

	static unsigned int hardware_concurrency() _NOEXCEPT {	    // 返回硬件线程上下文数量
		return (::Concurrency::details::_GetConcurrency());
	}

	native_handle_type native_handle() {	        // 以 void* 形式返回线程的 Win32 句柄
		return (_Thr._Hnd);
	}

private:
	thread& _Move_thread(thread& _Other) {	        // move from _Other
		if (joinable())
			_XSTD terminate();
		_Thr = _Other._Thr;
		_Thr_set_null(_Other._Thr);
		return (*this);
	}

	_Thrd_t _Thr;            // 私有成员变量,_Thrd_t是一个结构体,后面会分析
};

源码分析

成员变量

        先来看thread类中唯一的一个私有成员变量,在代码中提到了它是一个结构体,看下面的定义就明了了:

_Thrd_t _Thr; //其实_Thrd_t 是类型的别名

typedef _Thrd_imp_t _Thrd_t;    // 而_Thrd_imp_t是一个结构体

typedef struct {	/* 线程 Win32 标识符 */
    void *_Hnd;	    /* Win32 句柄 */
    unsigned int _Id;    // 线程id
} _Thrd_imp_t;
        到这里,我想大家心中终于有点着落了吧。

成员方法

        现在来剖析剩下的thread方法。


1、thread::joinable()方法,其定义如下:

bool joinable() const _NOEXCEPT {       // 若线程可结合程,返回 true;否则,返回flase
    return (!_Thr_is_null(_Thr));       // 宏定义,原型为:#define _Thr_is_null(thr) (thr._Id == 0)
}

该方法判断线程是否可结合,实质就是判断线程id是否为0。


2、thread::join()方法,其定义如下:

inline void thread::join(){	// join thread
	if (!joinable())            // 线程不可结合
		_Throw_Cpp_error(_INVALID_ARGUMENT);
	if (_Thr_is_null(_Thr))     // 空线程
		_Throw_Cpp_error(_INVALID_ARGUMENT);
	if (get_id() == _STD this_thread::get_id()) // 线程不能与自己结合
		_Throw_Cpp_error(_RESOURCE_DEADLOCK_WOULD_OCCUR);
	if (_Thrd_join(_Thr, 0) != _Thrd_success)   // 线程结合(_Thrd_join()是join方法的核心),是阻塞的
		_Throw_Cpp_error(_NO_SUCH_PROCESS);
	_Thr_set_null(_Thr);        // 设置线程id为0
}
由上述代码和注释可以知道,在以下几种情况下,线程是不可结合的:

        ① 线程已经join()过了;

        ② 线程为空线程;

        ③ 单个的线程,也就是线程自己与自己;

如果一个可结合的线程经过join后(等线程执行完毕后),会将线程id置为0。


3、thread::detach()方法,定义如下:

void detach() {	// detach thread
    if (!joinable())        // 线程不可结合
        _Throw_Cpp_error(_INVALID_ARGUMENT);
    _Thrd_detachX(_Thr);    // 线程分离(detach的核心)
    _Thr_set_null(_Thr);    // 设置线程id为0
}

好了,这里几个比较重要的方法和概念就分析完毕了。接下来介绍一下构造函数、析构函数及其他函数,最后会来总结一下。


4、析构函数,定义如下:

~thread() _NOEXCEPT {	    // 析构函数
    if (joinable())         // 线程是可结合的,析构异常(也就是说只能析构不可结合的线程,即id为0线程;id不为0的线程不能析构)
        _XSTD terminate();  // terminate会调用abort()来终止程序
}

这个如果存在疑问,待会儿请看后面的总结。


5、构造函数

thread() _NOEXCEPT {	    // 构造函数,空线程
    _Thr_set_null(_Thr);    // 宏定义,原型为:#define _Thr_set_null(thr) (thr._Id = 0)
}

template<class _Fn, class... _Args>
explicit thread(_Fn&& _Fx, _Args&&... _Ax) {	// 带参模板构造函数_Fx(_Ax...)
    _Launch(&_Thr, _STD bind(_Decay_copy(_STD forward<_Fn>(_Fx)), _Decay_copy(_STD forward<_Args>(_Ax))...));
}

thread(thread&& _Other) _NOEXCEPT : _Thr(_Other._Thr) {	    // 拷贝构造函数,调用move
    _Thr_set_null(_Other._Thr);
}

thread& operator=(thread&& _Other) _NOEXCEPT {	// 赋值函数,调用move
    return (_Move_thread(_Other));
}

thread(const thread&) = delete;                 // 禁用 拷贝构造函数
thread& operator=(const thread&) = delete;      // 禁用 赋值函数

对于构造函数和赋值函数,下面举几个列子大家就明白了:

int n = 20;
thread t1, t2;          // 正确,空线程

// 带参的构造函数,先写线程执行的函数,后面有多少参数就跟多少个
thread t3(task_one);    // 正确,0个参数
thread t4(task_two, n); // 正确,1个参数

thread t5(t3);          // 错误,使用了被禁用的拷贝构造函数
thread t6 = t4;         // 错误,使用了被禁用的赋值函数

thread t7(move(t3));    // 正确,使用move,t7与t3功能相同,但t3被move之后变成了空线程
thread t8 = move(t4);   // 正确,使用move,t8与t3功能相同,但t3被move之后变成了空线程


6、其他方法

        ① thread::get_id()方法,获取线程id;其定义如下:

inline thread::id thread::get_id() const _NOEXCEPT {
	return (id(*this));
}

        由于之前提到过id是个内部类,这个后面再分析。


        ② thread::swap()方法,线程交换;其定义如下:

void swap(thread& _Other) _NOEXCEPT {
    _STD swap(_Thr, _Other._Thr);
}

template<class _Ty> inline
void swap(_Ty& _Left, _Ty& _Right) _NOEXCEPT_OP(is_nothrow_move_constructible<_Ty>::value && is_nothrow_move_assignable<_Ty>::value) {
    _Ty _Tmp = _Move(_Left);
    _Left = _Move(_Right);
    _Right = _Move(_Tmp);
}


        ③ thread::hardware_concurrency()方法,这是个静态方法,返回的是硬件线程上下文数量;其定义如下:

static unsigned int hardware_concurrency() _NOEXCEPT {
    return (::Concurrency::details::_GetConcurrency());
}

       

        ④ thread::native_handle()方法,获取线程的win32句柄;其定义如下:

native_handle_type native_handle() {    // 其中:typedef void *native_handle_type;
    return (_Thr._Hnd);
}


最后还有一个私有方法,这个方法在拷贝构造函数(使用move)中调用。

        ⑤ thread::_Move_thread()方法,其定义如下:

thread& _Move_thread(thread& _Other) {	// move from _Other
    if (joinable())
        _XSTD terminate();
    _Thr = _Other._Thr;
    _Thr_set_null(_Other._Thr);    //线程id置为0
    return (*this);
}


总节

        最后,我们来总结一下,其实重点要理解的就是线程的join、detach、joinable三者的关系:

        我们再从thread的析构函数~thread()入手分析。

~thread() _NOEXCEPT {	    // 析构函数
    if (joinable())         // 线程是可结合的,析构异常(也就是说只能析构不可结合的线程)
        _XSTD terminate();  // terminate会调用abort()来终止程序
}
        其实析构函数里面只进行了判断,并没有析构什么,因为thread成员变量不存在用new或malloc进行内存分配的指针或数组,所以析构函数里不做资源释放工作。那么为什么只能析构不可结合的线程呢?

        这里还是以博客最开始的代码来分析,有主线程、t1、t2三个线程,如下。

int main() {
    int n = 20;

    thread t1(task_one);
    thread t2(task_two, n);

    t1.join();
    t2.join();

    cout << "main thread" << endl;
    return 0;
}

        我们可以总结一下线程不可结合(即joinable()为false)的几种情况:

               ① 空线程;

                ② move后的线程(即move(t),则t是不可结合的);

               ③ join后的线程;

                ④ detach后的线程;

        在实例化了t1、t2对象之后,它们的状态默认都是可结合的,如果现在直接调用它们的析构函数来析构它们,那么在析构的时候线程处于什么状态呢?是执行完了吗?还是正在执行呢?注意,如果一个在没有结合(join)的情况下,就算它先于主线程执行完毕,其id依然是不为0的。所以我们是不能确定其状态的,所以我们只能析构明确了id为0的线程。因为id为0的线程要么已经执行完毕,要么是空线程,要么是分离后的线程。

        另外,一个线程分离(detech)后,该线程对象边便不能控制该线程,而是交由系统接管。


        今天就到这里。上面有些理解都是个人的理解和看法,或许有词不达意或者错误的地方,希望大家能够多多指教,谢谢!

  • 15
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值