线程的启动
线程在以初始函数(新线程的入口)构造std::thread对象时启动,换句话说,合理的构造std::thread即启动线程
(注意:以无参构造函数构造的std::thread对象并不启动线程)
构造std::thread对象的三种方式
使用普通函数构造
void do_some_work();
std::thread my_thread(do_some_work);
使用可调用对象构造
class background_task
{
public:
void operator()() const
{
do_something();
do_something_else();
}
};
background_task f;
std::thread my_thread(f);
使用类成员函数与类对象构造
class Test
{
public:
void testFunc();
};
Test myTest;
std::thread t(&Test::testFunc, &myTest);
注意:
1、使用以函数对象构造std::thread对象的方式时,需要注意避免直接传递临时对象(直接调用类的构造函数时产生的对象),因为当传递临时对象时,C++编译器会将其解析为函数声明,而不是类对象的定义,因此不会启动线程
std::thread my_thread(background_task()); //被视为声明一个参数为函数指针且返回值为std::thread对象的函数
解决方案
1)、使用命名对象而不是直接传递临时对象
background_task f;
std::thread my_thread(f);
2)、使用多组括号
std::thread my_thread( (background_task()) ); //额外的一对括号
3)、使用C++11新特性,同一初始化语法
std::thread my_thread{ background_task() }; //大括号
4)、使用C++11新特性lambda表达式
std::thread my_thread( []{ //捕获列表为空
do_something(); //参数列表与返回值类型为空
do_something_else(); //函数体为两个函数的调用
} );
Lambda表达式表示一个可调用的代码单元(像函数对象一样的可调用对象),可以将其理解为未命名的内联函数
线程启动之后
线程在启动之后我们可以对其选择两种操作:
1、使用join()明确等待线程结束(加入式)(有点像pthread_join())
2、使用detach()让其自主运行(分离式)(有点像pthread_detach())
注意
1、在std::thread对象析构之前,无论是分离还是加入,必须为其选择一种操作,否则程序就会终止(std::thread对象的析构函数会调用std::terminate()函数)
2、如果线程已经结束(指线程入口函数全部执行完毕),再去分离它,线程(线程入口函数)可能会在std::thread对象销毁后继续运行下去(测试发现并不是这样,或许没碰到我期望的可能吧)
3、join()方式会使当前线程阻塞直到线程执行完毕(当前线程为了回收线程的资源),而detach()方式则直接分离了当前线程与线程,当前线程不会阻塞直到线程结束,使用detach()方式整体来说更为高效,但是使用这种方式必须要谨慎
必须保证线程在结束之前,其可以访问到的数据有效!!!!!
比如线程持有当前线程函数中的局部变量/对象,但是当前线程比线程先结束,这时,线程所持有的指针指向或引用的变量/对象已经无效,这非常危险!
注:当前线程是指构造std::thread对象的线程
struct func
{
int& i;
func(int& i_) : i(i_) {}
void operator() ()
{
for(unsigned j=0;j<1000000;++j)
{
do_something(i); //危险
}
}
};
void oops()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.detach(); //当前线程可能先于线程结束
}
等待线程完成
Join()是简单粗暴的等待线程完成(阻塞),一旦join()成功,std::thread对象将不再与已经完成的线程有任何关系。
应注意只能对std::thread对象使用一次join(),每次使用joinable()判断后再join()
if( t.joinable() )
{
t.join();
}
合适的Join()的位置
如果确定需要等待线程结束,则应该仔细考虑join()的位置,join()不应该被某种原因影响而跳过执行
struct func; //函数对象
void f()
{
int some_local_state=0;
func my_func(some_local_state);
std::thread t(my_func);
try
{
do_something_in_current_thread();
}
catch(...)
{
t.join();
throw;
}
t.join();
}
若确定要等待线程,则务必确保线程在当前线程之前结束
RAII_1
有一个简洁的机制可以让我们不必总是寻找在何处join(): 利用RAII机制RAII(Resource Acquisition Is Initialization)”资源取得实际便是初始化时机”,即”以对象管理资源”,使线程刚被启动时就被对象所管理,并在析构函数中使用join()
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;
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();
}
由于析构的顺序,g对象会被先析构,线程在g析构时即join()
后台运行线程
使用detach()会让线程在后台运行,即会分离线程,分离后,想要的std::thread对象与实际执行的线程无关了,而且被分离的线程不能被加入join()
线程被分离后,当当前线程退出时,线程的相关资源的回收、线程的归属与控制由C++运行库处理
由于不能对没有执行线程的std::thread对象(默认构造函数构造的对象)与已分离的对象使用detach(),因此使用detach()之前也需要使用joinable()判断,如果返回true,则可以detach()
向线程传递参数
首先,知晓一个概念对理解传参非常有帮助 :
std::thread 的构造函数会拷贝提供的变量/对象
其实这个大家都知道的,构造函数也是函数,函数传参除了传递引用都会存在拷贝行为(包括传地址,传地址是拷贝的地址值)
传参的三种方式
向由普通函数启动的线程传参
void f(int i, std::string const& s);
std::thread t(f, 3, "hello"); //3、以及字符串字面值的地址被拷贝
如果仔细看了上面的总结,当看到向线程传递地址或引用的动作时你应该有所警觉!因为这一动作可能会违反” 必须保证线程在结束之前,其可以访问到的数据有效”这一规则,值得庆幸的是字符串常量”hello”本身位于常量区,所以无论线程被加入还是分离都不会违反这一规则。
下面的例子则不然
void f(int i,std::string const& s);
void oops(int some_param)
{
char buffer[1024];
sprintf(buffer, "%i",some_param);
std::thread t(f,3,buffer);
t.detach();
}
上面的代码中,定义时buffer为栈数组,在std::thread对象构造时,构造函数得到的实际是指向栈数组首元素首地址的指针,是个指向局部变量的指针!并且线程是分离方式!这非常危险!
解决方案
在传递到std::thread构造函数之前将其转化为std::string对象
void f(int i,std::string const& s);
void not_oops(int some_param)
{
char buffer[1024];
sprintf(buffer,"%i",some_param);
std::thread t(f,3,std::string(buffer));
t.detach();
}
还记得吗? std::thread 的构造函数会拷贝提供的变量/对象,此处用生成的临时std::string对象给线程传参,虽然临时对象也是非常危险的,它只能存在于它产生的所在行,but,临时对象刚一生成就被函数传参这一动作所拷贝。即使在执行到下一句临时对象被销毁,其拷贝(函数形参)依旧存在。即使线程被分离,线程获得的参数也是一份拷贝(准确的说是拷贝的拷贝第一次拷贝发生在std::thread的构造函数构造时,第二次拷贝发生在将拷贝传递给线程入口函数时),并不会存在什么危险。
一个新问题?
有时候,在线程中改变当前线程中的变量值好像也是必要的,如果想要这样,我们必须保证
的东西已经反复强调过了,具体的方法是要么使用生命周期长于线程生命周期的变量,要么使用join()阻塞直到线程结束。
好了,做好了保证的东西之后怎么在线程中改变当前线程中的值呢?我之前有说过,线程的入口函数其实是得到的拷贝的拷贝(最开始仅仅是我的猜想,经测试,确实如此)
测试代码如下
#include <thread>
#include <cstdlib>
#include <iostream>
void hello_(int times, int j)
{
j = 1000;
for(int i = 0; i < times; ++i)
{
std::cout << "for" << std::endl;
}
std::cout << "hello_:: j = " << j << std::endl;
}
void hello_2(int times, int &j)
{
j = 1000;
for (int i = 0; i < times; ++i)
{
std::cout << "for" << std::endl;
}
std::cout << "hello_2:: j = " << j << std::endl;
}
int main(int argc, char **argv)
{
int t1_num = 10;
int t2_num = 10;
std::thread test(hello_, 1, std::ref(t1_num));
std::thread test2(hello_2, 1, t2_num);
if( test.joinable() )
test.join();
if( test2.joinable() )
test2.join();
std::cout << "t1_num = " << t1_num <<std::endl;
std::cout << "t2_num = " << t2_num <<std::endl;
system("pause");
return 0;
}
运行结果如下:
注:linux下测试需要指定以C++11方式编译并指定连接线程库
由于发生了两次拷贝,为了能在线程内部改变当前线程中的值,需要通过下面的方式之一:
1、std::thread对象构造时向构造函数传递变量的地址,并将线程入口函数中形参声明为指针
2、std::thread对象构造时向构造函数传递变量的地址,并将线程入口函数中形参声明为指针的引用
3、构造std::thread对象时,使用std::ref()(C++11新特性)将参数转换成引用形式,并将线程入口函数中形参的声明为引用
如果我没有遗漏的话,应该没有其他方式可以在线程中改变当前线程中的值了,另外值得注意一点的是,确保传参类型一致,在传参时编译器有类型检查,不一致会报错
向由函数对象启动的线程传参
还是这个 std::thread 的构造函数会拷贝提供的变量/对象 于是乎可以通过函数对象的成员变量向线程传参
struct func
{
int i;
func(int i_) : i(i_) {}
void operator() ()
{
for (unsigned j=0 ; j<i; ++j)
{
std::cout << j << std::endl;
}
}
};
void oops()
{
int some_local_state = 10;
func my_func(some_local_state);
std::thread my_thread(my_func);
my_thread.detach();
}
向由类成员函数启动的线程传参
当以此种方式向线程传参时,std::thread的构造函数的第一个参数就是类的成员函数,第二个参数就是类对象,第三个参数开始往后的参数为成员函数需求的参数
class X
{
public:
void do_lengthy_work(int);
};
X my_x;
int num(0);
std::thread t(&X::do_lengthy_work, &my_x, num);
注意到了吗?,构造函数的第二个参数是指向对象的指针!由此,你应该想到些什么!
转移线程所有权
C++标准库中很多资源占用类型,如std::ifstream,、std::unique_ptr、std::thread都是可移动不可拷贝的,也就是说线程的所有权可以在多个std::thread对象中移动
转移意味着原对象将不再占用资源,即原std::thread对象将不再与线程相关联,原对象也将不能对线程进行加入或分离操作
注意
线程的所有权可以移动,但是一个std::thread对象只能拥有一个线程,再次向一个std::thread对象转移线程所有权的操作会终止程序的运行()
两种所有权转移的方式
显示转移
显示转移适用于有名对象之间转移线程所有权,使用时需要使用std::move()(C++11特性)
void some_function();
std::thread t1(some_function);
std::thread t2=std::move(t1);
注意:如果你非要使用std::move()将一个临时对象所关联的线程所有权转移给有名对象也是可以的,但反之将会引发程序的终止(因为这相当于直接丢弃一个线程)!
隐式转移
隐式转移是自动发生的,很多所有潜在的拷贝操作都将引发隐式转移,如拷贝构造、拷贝赋值、函数传参、函数返回。
t1=std::thread(some_other_function);
std::thread th (std::thread(&test::func, &t) ); //func为成员函数,t为类对象
std::thread g() //返回std::thread对象
{
void some_other_function(int);
std::thread t(some_other_function,42);
return t;
}
void f(std::thread t);
void g()
{
void some_function();
f(std::thread(some_function));
std::thread t(some_function);
f(std::move(t)); //显示转移
}
注意
就像我之前提到的,一个std::thread对象只能拥有一个线程,试图向一个std::thread对象再次转移线程所有权的操作将会使程序终止,不过不用担心两个已关联线程的对象间存在拷贝赋值操作,因为你这样做的话,编译器会提示
error C2280: 'std::thread &std::thread::operator =(const std::thread &)' : attempting to reference a deleted function
RAII_2
class scoped_thread
{
std::thread t;
public:
explicit scoped_thread(std::thread t_):
t(std::move(t_))
{
if( !t.joinable() )
throw std::logic_error(“No thread”);
}
~scoped_thread()
{
t.join();
}
scoped_thread(scoped_thread const&)=delete;
scoped_thread& operator=(scoped_thread const&)=delete;
};
struct func; //函数对象
void f()
{
int some_local_state;
scoped_thread t(std::thread(func(some_local_state)));
do_something_in_current_thread();
}
如果一个容器是移动敏感的容器( 如std::vector<> ),std::thread对象的移动操作同样适合
void do_work(unsigned id);
void f()
{
std::vector<std::thread> threads;
for(unsigned i=0; i < 20; ++i)
{
threads.push_back(std::thread(do_work,i)); // 产生线程
}
std::for_each(threads.begin(),threads.end(),
std::mem_fn(&std::thread::join)); // 对每个线程调用join()
}
一个相当有用的库函数
函数std::thread::hardware_concurrency()的返回值是在一个线程能真正并发的数量(硬件线程)或者返回0(当系统信息无法获取时)
借助这个库函数,也许我们能写出并发效率最高的代码
线程的识别
一个线程的标识是std::thread::id,可以通过两种方式获得一个线程的标识:
获取非当前对象所关联线程id
通过调用std::thread对象的成员函数get_id(),如果std::thread对象没有与任何线程关联,get_id()将返回std::thread::id的默认构造值(值为0),这个值表示没有线程
获取当前线程id
通过调用std::this_thread::get_id()函数
和std::thread对象不同,std::thread::id对象可以自由拷贝,还可以对比两个std::thread::id对象。
如果两个std::thread对象的std::thread::id相等,那么它们就是同一线程或都没有线程,如果不等,就代表了两个不同的线程,或者一个有线程,另一个没有。
线程id的作用
通过区别线程id来使线程执行不同的操作
std::thread::id master_thread;
void some_core_part_of_algorithm()
{
if( std::this_thread::get_id()==master_thread )
{
do_master_thread_work();
}
do_common_work();
}
注:文中的例代码大部分来自 C++并发编程实战
参考: C++并发编程实战
C++ Primer
Effective C++