文章目录
- 启动线程
- 传递函数对象为参数
- 传递成员函数为参数
- 传递全局函数为参数
- 传递lambda函数为参数
- 也可调用std::thread的无参构造
- join()、joinable()、detach() 等函数
- Join函数
- detach 函数
- joinable函数
- Join函数到底干了什么?
- 必须join或者detach吗?
- 线程资源不能被覆盖
在C++11之前的C++98/03标准是不支持的多线程的。想要使用多线程需要使用使用POSIX C 或者其他API功能。
在C++11标准中引入的支持多线程。想要使用C++11中的多线程,需要 添加
#include <thread>
。
在C++11标准库中对多线程支持以及用于管理线程的函数和类在
<thread>
中声明,而那些用于保护共享数据的函数和类在其他头文件中声明。
每个线程都必须具有一个初始函数(initial function),新线程的执行在这里开始。
初始线程始于 main(),而新线程始于与创建的thread对象拥有的初始函数。
启动线程
线程是通过构造 std::thread对象来开始的,该对象指定了线程上要运行的任务。在最简单的情况下,该任务仅仅是一个普普通通的返回 void 且不接受参数的函数。这个函数在自己的线程上运行,直到返回,然后线程停止。该任务也可以是一个 函数对象
,lambda表达式
,成员函数
。
传递函数对象为参数
#include <iostream>
#include <thread>
using namespace std;
class background_task_1
{
public:void operator()()const //无参 重载了operator()函数的类的实例称为函数对象{std::cout << "background_task_1::operator() ...." << endl;}void operator()(int a)const//有参 重载了operator()函数的类的实例称为函数对象{std::cout << "background_task_1::operator() .... a =" << a << endl;}
};
//
int main(int argc, char *argv[])
{//thread t1(background_task_1());//编译报错background_task_1 task1; //创建一个函数对象thread t1(task1);//会启动一个新线程执行函数对象task1里无参的operator()函数thread t2(task1,2);//会启动一个新线程执行函数对象task1里有参的operator()(int)函数t1.join();//等待t1线程(被调用线程)执行结束完,(调用线程)这里是主线程然后才会再结束。t2.join();//等待t1线程(被调用线程)执行结束完,(调用线程)这里是主线程然后才会再结束。//总之只要每个新启动的线程都调用join()方法,那么调用线程都会等待所有的线程执行完毕后,才会结束主线程。
}
打印结果:
注意
:thread t1(background_task_1()); 如这样传递一个临时的且未命名的变量,编译器会解释为如下:
声明了函数 my_thread,它接受单个参数(参数类型是指向不接受参数同时返回background_task_1对象的函数的指针),
并返回thread 对象。而不是启动一个新线程。
编译器无法分清该行是要调用构造函数还是定义变量,从而导致出现 parentheses were disambiguated as a function declaration 警
告。
总之 在传递匿名对象做为实参时,需注意"C++最棘手的解析"。
可以使用以下两种方式
1. 使用强制类型转换
thread t1((
background_task_1())
);
方式1中额外的括号避免其解释为函数声明,从而让 t1被声明为std::thread类型的变量。
2.使用{}调用构造函数 C++1
thread t1{
(background_task_1()}
;
方式2中使用了新的统一初始化语法,用大括号而不是括号,同样也是声明一个变量。
传递成员函数为参数
class background_task_2
{
public:void member_func(){std::cout << "background_task_2::member_func .... " << endl;}void member_func1(){std::cout << "background_task_2::member_func1 ...."<< endl;}void member_func1(int b){std::cout << "background_task_2::member_func1 .... b = " << b << endl;}
};int main(int argc, char *argv[])
{background_task_2 task2;thread t1(&background_task_2::member_func,&task2);//member_func 为当前类中唯一此名的函数,不存在重载可以这样传递。t1.join();return 0;
}int main(int argc, char *argv[])
{background_task_2 task2;//member_func1函数存在重载的情况下void (background_task_2::*fun1)() = &background_task_2::member_func1;void (background_task_2::*fun2)(int) = &background_task_2::member_func1;thread t2(fun1,&task2);//启动一个新线程,执行member_func1无参函数thread t3(fun2,&task2,80);//启动一个新线程,执行member_func1有参函数t2.join();t3.join();return 0;
}
传递全局函数为参数
void global_func(int a)
{std::cout << "global_func.... a = " << a << endl;
}int main(int argc, char *argv[])
{thread t1(global_func,30);//函数名,是第一个参数,函数中的参数,依次往后写.t1.join();return 0;
}
传递lambda函数为参数
int main(int argc, char *argv[])
{//thread t1(global_func,30);int a = 10;int b = 20;thread t1([=]{cout << "a + b = " << a+b << endl;});t1.join();return 0;
}
也可调用std::thread的无参构造
int main(int argc, char *argv[])
{thread t1;//单独使用它,没有意思,因没有绑定线程函数。一般用于接收其他线程对象。t1.join();//当t1是根据无参构造生成的对象,直接调用join会报错,后面讲joinable时用的
}
join()、joinable()、detach() 等函数
因为一旦开始线程,你需要显示地决定是要等待它完成(通过使用join等待它),还是让它自行运行(通过detach分离它)。
如果在std::thread对象销毁前未做决定,那么你的程序会被终止(thread对象的析构函数调用std::terminate())。
C++11的多线程的执行和Java的多线程的执行不太一样。Java中的多线程不用指定结合它 还是 分离它。
Join函数
Join
(等待):
当主线程调用了 join
方法后,它会阻塞直到子线程完成并返回控制权。
主线程在执行 join
后,通常会继续执行后续代码,因为它已经知道子线程的状态。
如果子线程抛出异常,那么主线程会在异常被处理之前接管控制权。
使用 join
时,如果子线程没有完成,主线程将一直处于阻塞状态直到子线程结束。
int main(int argc, char *argv[])
{//thread t1(global_func,30);int a = 10;int b = 20;thread t1([=]{cout << "a + b = " << a+b << endl;});t1.join();//主线程会阻塞到这里,等待子线程执行完毕后,再执行下面一句cout << "main..." << endl;return 0;
}
另外一个例子:
假设有两个线程,线程A和线程B。线程A被托管在thread对象A中。在线程B中执行对象A的join()函数,那么线程B就会被阻塞住,直到线程A执行完成后,线程B才会执行A.join()后面的代码。
//使当前线程睡眠的封装函数
void Sleep(long ms){std::this_thread::sleep_for(std::chrono::milliseconds(ms));
}void f(){Sleep(1000);//会休眠1秒cout<<"I am f thread"<<endl;
}int main()
{thread t1(f);//线程A被托管在thread对象t1中t1.join();//线程B 指的就是 mian函数主线程cout<<"run main thread"<<endl;
}
但是如果创建一个空的thread对象
int main()
{thread t1;t1.join();cout<<"run main thread"<<endl;
}
空的thread对象,其实是没有持有线程资源,这种情况是不能join的。
detach 函数
Detach
(分离):
detach
操作使得子线程不再需要与主线程同步,它可以独立地执行自己的任务。
子线程一旦进入终止状态,无论是否使用了 detach
,都会自动从系统中移除。
使用 detach
的线程通常不会被主线程回收资源或杀死,除非有其他线程尝试进行这些操作。
detach
通常用于多线程编程中,减少主线程因为等待子线程而停滞的时间。
总结来说,join
是用来等待并接收子线程的控制权的,而 detach
是用来使子线程成为独立的进程,不再需要主线程的管理。这两种方法适用于不同的情况,选择哪一种取决于你的需求。
int main(int argc, char *argv[])
{//thread t1(global_func,30);int a = 10;int b = 20;thread t1([=]{cout << "a + b = " << a+b << endl;});t1.detach();//无须等待子线程的执行完毕,主线程会立即执行下一句cout << "main..." << endl;return 0;
}
joinable函数
Joinable
(可结合)
joinable()函数的功能,官方文档说的非常晦涩难懂。我个人比较倾向于这种解释:" 判断当前的线程对象是否是可执行线程对象。"
一般有三种情况,joinable()返回false:
(1)该thread对象通过无参构造函数构造;
(2)该thread对象执行过join()或者detach();
(3)该thread对象被move过,但是move的接受方的joinable()结果为true。
void f5(){};void f5(){};int main()
{thread t1;thread t2(f5);cout<<"t2 : "<<t2.joinable()<<endl;t1=move(t2);//调移动构造函数cout<<"t1 : "<<t1.joinable()<<endl;cout<<"t2 : "<<t2.joinable()<<endl;t1.join();
};
注意:
t2在这里把线程资源权移交给了t1,所以t2在被析构时,其joinabe() 返回false。所以不用使用t2.join() 或者 t2.detach();时也可以正常被析构。不会报 terminate called without an active exception。
Join函数到底干了什么?
一个非常容易忽略的点:join函数实际上是放弃了持有的线程资源。对于没有持有线程资源的thread对象不能join,这是为了保证thread对象只能join一次。joinable函数实际上是检测thread对象是否持有线程资源。
假如说我们创建了一个空的thread对象,没有传入函数,自然不会持有操作系统层面的线程资源。强行join就会报错。
假如我们给它move进来一个资源以后,那么它就可以join了。
这个对象在执行了join函数以后,彻底放弃了对于线程资源的管理权,joinable状态变成了false。
同样的道理,thread被move以后,放弃了线程资源,joinable状态变成了false。
一句话:joinable()返回值为true的对象,也就是持有线程资源的对象,才能join。
必须join或者detach吗?
从宏观上看,是的。
一句话描述:C++程序中,在thread对象执行析构函数的时候,不允许持有线程资源。执行析构之前,要么通过join放弃资源的控制权,要么将线程资源move出去。
std::thread类的析构源码
~thread() _NOEXCEPT
{ // clean upif (joinable())_XSTD terminate();
}
即 当系统在调当前线程对象的析构函数时,若joinable()返回true(表明当前线程即没有join或没有detach 或没有移交线程资源),则会调terminate() 终止程序。
线程资源不能被覆盖
void f6(){
}
int main()
{thread t1(f6);thread t2(f6);t1=move(t2); //覆盖t1持有的线程资源cout<<"t1 : "<<t1.joinable()<<endl;cout<<"t2 : "<<t2.joinable()<<endl;t1.join();
}
可以看到当t1持有线程资源时,是不能被t2的线程资源覆盖的。