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
}
好了,这里几个比较重要的方法和概念就分析完毕了。接下来介绍一下构造函数、析构函数及其他函数,最后会来总结一下。
~thread() _NOEXCEPT { // 析构函数
if (joinable()) // 线程是可结合的,析构异常(也就是说只能析构不可结合的线程,即id为0线程;id不为0的线程不能析构)
_XSTD terminate(); // terminate会调用abort()来终止程序
}
这个如果存在疑问,待会儿请看后面的总结。
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)后,该线程对象边便不能控制该线程,而是交由系统接管。
今天就到这里。上面有些理解都是个人的理解和看法,或许有词不达意或者错误的地方,希望大家能够多多指教,谢谢!