第二章 管理线程
线程管理基础
启动线程
线程在构造 std: :thread对象时启动,这个对象指定了要运行的任务
std::thread类的构造函数是使用可变参数模板实现的,也就是说,可以传递任意个参数,第一个参数是线程的入口函数,而后面的若干个参数是该函数的参数。
第一参数的类型并不是c语言中的函数指针(c语言传递函数都是使用函数指针),在c++11中,增加了可调用对象(Callable Objects)的概念,总的来说,可调用对象可以是以下几种情况:
函数指针
重载了operator()运算符的类对象,即仿函数
lambda表达式(匿名函数)
std::function
- 函数指针
// 普通函数 无参
void function_1() {
}
// 普通函数 1个参数
void function_2(int i) {
}
// 普通函数 2个参数
void function_3(int i, std::string m) {
}
std::thread t1(function_1);
std::thread t2(function_2, 1);
std::thread t3(function_3, 1, "hello");
t1.join();
t2.join();
t3.join();
注意:如果将重载的函数作为线程的入口函数,会发生编译错误!编译器搞不清楚是哪个函数
- 重载了operator()运算符的类对象,即仿函数
#include <iostream>
#include <thread>
using namespace std;
class obj
{
public:
void operator()()//暂时不带参数 线程入口点
{
cout << "线程开始执行" << endl;
//...
cout << "线程执行结束1" << endl;
}
};
int main()
{
obj ob;
thread mytobj(ob);
mytobj.join();
cout << "hhh" << endl;
return 0;
}
//类内声明了变量的情况
class obj
{
public:
int &m_i1;//&m_i1,&m用了&,detach程序会出问题,去掉引用才可以detach
obj(int &m) :m_i1(m) {}
void operator()()//暂时不带参数 线程入口点
{
cout << "线程开始执行" << endl;
//...
//m是主函数的局部变量的引用,当主线程执行完毕之后,局部变量被释放,此时访问m_i1,结果不可预料
cout << "m_i1 1"<<m_i1 << endl;
cout << "m_i1 2" << m_i1 << endl;
cout << "m_i1 3" << m_i1 << endl;
cout << "m_i1 4" << m_i1 << endl;
cout << "m_i1 5" << m_i1 << endl;
cout << "m_i1 6" << m_i1 << endl;
}
};
int main()
{
int m = 2;
obj ob(m);
//主线程运行结束后,m被释放,会产生不可预料后果,那对象ob也被释放了啊?子线程为什么还存在?
//原因:对象ob是被 复制 到线程中去的,主线程中的ob被释放,但是线程中的obj对象还存在。但是如果这个对象使用了指针引用之类的,依旧会存在问题
//可以自己写一个拷贝构造函数证明,此处复制
thread mytobj(ob);
//thread mytobj1(obj());//错误,如果传递了一个临时变量,而非命名变量,编译器会将其解析为函数声明,而不是对象的定义。
//解决办法:
thread mytobj1((obj()));
thread mytobj2{obj()};
mytobj.detach();
return 0;
}
注意:如果传递了一个临时变量,而非命名变量,编译器会将其解析为函数声明,而不是对象的定义。解决办法就是在临时变量外包一层小括号(),或者在调用std::thread的构造函数时使用{}
如果重载的operator()运算符有参数,就不会发生上面的错误。
- lambda表达式(匿名函数)
int main()
{
auto lambdaThread = [] {
cout << "我的线程开始执行了" << endl;
//-------------
//-------------
cout << "我的线程开始执行了" << endl;
};
thread myThread(lambdaThread);
myThread.join();
cout<<"hhh"<<endl;
return 0;
}
- std::function
C++11中新引入的模板类。类模板std::function是一种通用的多态函数包装器。std::function可以存储,复制和调用任何Callable 目标的实例,例如函数,lambda表达式,绑定表达式或其他函数对象,以及指向成员函数和指向数据成员的指针。
个人理解,fonction的作用就是把可调用对象转为统一形式,便于管理
class A{
public:
void func1(){
}
void func2(int i){
}
void func3(int i, int j){
}
};
A a;
std::function<void(void)> f1 = std::bind(&A::func1, &a);
std::function<void(void)> f2 = std::bind(&A::func2, &a, 1);
std::function<void(int)> f3 = std::bind(&A::func2, &a, std::placeholders::_1);
std::function<void(int)> f4 = std::bind(&A::func3, &a, 1, std::placeholders::_1);
std::function<void(int, int)> f5 = std::bind(&A::func3, &a, std::placeholders::_1, std::placeholders::_2);
std::thread t1(f1);
std::thread t2(f2);
std::thread t3(f3, 1);
std::thread t4(f4, 1);
std::thread t5(f5, 1, 2);
等待线程完成
如果需要等待线程完成,可以调用std::thread实例的join()函数。在实际编程中,原始线程要么有自己的工作要做;要么会启动多个线程做一些有用的工作,并等待这些线程结束。
join()是简单粗暴的技术一一要么等待线程完成,要么不等待。如果需要更细粒度的控制等待线程的过程,比如,检查某个线程是否结束,或者只等待一段时间。则要使用其他机制来完成,比如条件变量和期待(futures)。调用join()的动作,还清理了线程相关的存储,这样std::thread对象将不再与已经结束的线程有任何关联。这意味着,对一个线程只能调用一次 join();一旦已经调用过join(),std :: thread对象就不能再次连接,同时joinable()将返回false。
异常场景下的等待
如果打算等待线程,则需要细心挑选调用join()代码的位置。因为如果在线程启动后调用join()前有异常抛出,join()调用很容易被跳过。
解决方式一:为了避免应用因为异常抛出而终止,就需要作出一个决定。通常,当倾向于在无异常的情况下使用join()时,你也需要在异常处理过程中调用join(),从而避免意外的生命周期的问题。即在catch模块中throw前join()一下;确保程序结束前jion()过。
解决方式二:使用标准的“资源获取即初始化”(RAII,Resource Acquisition IsInitialization),并且提供一个类,在析构函数中执行 join(),如同下面清单中的代码。
//使用RAII等待线程完成
class thread_guard
{
std::thread& t;
public:
explicit thread_guard(std::thread& t_):t(t_)
~thread_guard(){
if(t.joinable()){
t.join();}
}
thread_guard(thread_guard const&)=delete;//禁用默认拷贝构造函数
thread_guard& operator=(thread_guard const&)=delete;//禁用默认赋值函数
};
struct func;//一个函数,书本清单2.1
void f()
{
int some_local_state=0;
func my_func(some_local_state);std: :thread t(my_func);
thread_guard g(t);
do_something_in_current_thread();
}
&emsps; 拷贝构造函数和拷贝赋值操作被标记为=delete,是为了不让编译器自动生成它们。拷贝或者赋值这类对象是危险的,因为它可能超过它所连接线程的作用域而继续存在。通过删除声明,任何尝试给thread_guard对象赋值的操作都会引发一个编译错误。
在后台运行线程
调用detach()会让线程在后台运行,这就意味没有直接的方法和他通信。也就没法等待线程结束;如果线程被分离了,那就不可能有std::thread对象能引用它,所以也不能被连接。分离的线程的确在后台运行;所有权和控制会传递给C++运行时库,它会保证和线程相关的资源在线程退出的时候被正确的回收。
分离线程经常叫做守护线程(daemon threads)这是参照UNIX中守护进程的概念,这种运行在后台的进程没有任何显式的用户接口。这种线程的特点是长时间运行;它们运行在应用的整个生命周期中,可能会在后台监视文件系统,还有可能清理没用的对象缓存,亦或优化数据结构。在另一个极端,分离线程也有用武之地,比如用在有另一种机制标识线程什么时候完成的地方,或者用于发后即忘(fire and forget)的任务。
例子:考虑一个应用程序比如文字处理器能同时编辑多个文档。在UI和内部实现有很多种处理方法。目前普遍的一种方式是用多个独立的顶层窗口,每个文档在编辑的时候对应一个。尽管这些窗口看起来完全独立,每个有它自己的菜单,但他们运行在应用的同一个实例。一种内部处理方式是,让每个文档处理窗口拥有自己的线程;每个线程运行同样的代码,但被编辑的文档有不同的数据,以及配套的窗口属性。打开一个文档就要启动一个新线程。处理请求的线程没有兴趣等待其它线程结束,因为它工作在跟别人无关的文档上。因此,这使得运行分离的线程变成首选。
//使用分离线程去处理其他文档
void edit_document(std: : string const& filename)
{
open_document_and_display_gui(filename);
while( !done_editing())
{
user_command cmd=get_user_input();
if(cmd.type==open_new_document)
{
std::string const new_name=get_filename_from_user();
std::thread t(edit_document, new_name);// 1
t.detach();// 2
}
else
{
process_user_input( cmd);
}
}
}
传递参数给线程函数
向可调用对象或函数传递参数很简单,只需要将这些参数作为std::thread构造函数的附加参数即可。但需要注意的是,在默认情况下,这些参数会被拷贝至新线程的内部存储,新创建的执行线程可以访问它们,然后像临时值那样以右值引用传递给调用对象或者函数。就算函数的参数期待一个引用时仍然是这样。
#include <iostream>
#include <thread>
#include <string>
// 仿函数
class Fctor {
public:
// 具有一个参数 是引用
void operator() (std::string& msg) {
msg = "wolrd";
}
};
int main() {
Fctor f;
std::string m = "hello";
std::thread t1(f, m);
t1.join();
std::cout << m << std::endl
return 0;
}
// vs下: 最终是:"hello"
// g++编译器: 编译报错
子线程并没有成功改变外面的变量m.
如果可以想真正传引用,可以在调用线程类构造函数的时候,用std::ref()包装一下。std::thread t1(f, std::ref(m));
后果:多个线程同时修改同一个变量,会发生数据竞争。
同理,构造函数的第一个参数是可调用对象,默认情况下其实传递的还是一个副本。
#include <iostream>
#include <thread>
#include <string>
class A {
public:
void f(int x, char c) {}
int g(double x) {return 0;}
int operator()(int N) {return 0;}
};
void foo(int x) {}
int main() {
A a;
std::thread t1(a, 6); // 1. 调用的是 copy_of_a()
std::thread t2(std::ref(a), 6); // 2. a()
std::thread t3(A(), 6); // 3. 调用的是 临时对象 temp_a()
std::thread t4(&A::f, a, 8, 'w'); // 4. 调用的是 copy_of_a.f()
std::thread t5(&A::f, &a, 8, 'w'); //5. 调用的是 a.f()
std::thread t6(std::move(a), 6); // 6. 调用的是 a.f(), a不能够再被使用了
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
t6.join();
return 0;
}
对于线程t1来说,内部调用的线程函数其实是一个副本,所以如果在函数内部修改了类成员,并不会影响到外面的对象。只有传递引用的时候才会修改。所以在这个时候就必须想清楚,到底是传值还是传引用!
转移线程所有权
线程对象只能移动不可复制
线程对象之间是不能复制的,只能移动,移动的意思是,将线程的所有权在std::thread实例间进行转移。
void some_function();
void some_other_function();
std::thread t1(some_function);
// std::thread t2 = t1; // 编译错误
std::thread t2 = std::move(t1); //只能移动 t1内部已经没有线程了
t1 = std::thread(some_other_function); // 临时对象赋值 默认就是移动操作
std::thread t3;
t3 = std::move(t2); // t2内部已经没有线程了
t1 = std::move(t3); // 程序将会终止,因为t1内部已经有一个线程在管理了
运行时选择线程数量
C++标准库中的std::thread : : hardware_concurrency()函数会返回一个数量指示,表明执行程序真正可以并发的线程数。在一个多核系统中,返回值可以是CPU的核数。返回值也仅仅是个提示,当系统信息无法获取时,函数也可能返回0,但是,在线程间划分任务的时候它是个非常有用的指南。
标志线程
线程标识类型为std::thread::id,并可以通过两种方式进行检索。第一种,可以通过调用std::thread对象的成员函数get_id()来获取。如果std::thread对象没有与任何执行线程相关联,get_id()将返回默认构造的std:: thread::id对象,表示“没有任何线程”(“not any thread”)。第二种,调用std::this_thread:: get_id()也可以获得当前线程的标识,这个函数也定义在头文件中。
std : : thread::id类型的对象可以随意的拷贝和比较,否则的话作为标识作用就不大。如果两个std: : thread::id类型的对象相等,那它们代表了同一个线程,或者都持有“没有任何线程”(not any thread)的值。如果不相等,那么就代表了两个不同线程,或者一个代表了某个线程,另一个持有“没有任何线程”的值。
参考