关于std::thread的二三事
写在前面
很早之前写过一篇关于线程池文章,《基于C++11实现线程池》,一直被网友翻出来,对里面的一些细节实现提了许多问题。其实更推荐大家去阅读后来写的另外一篇关于线程池的文章,《当我谈线程池时我谈些什么——线程池学习笔记》。原因很简单,写第一篇线程池文章的时候,自己属实是菜的不行(现在依旧是菜的不行),花了很长时间去学习一些里面用到的C++11的新语法,文章里面讲的重点也是涉及到的C++11语法。所以,更关心线程池本身的实现的话,更适合去看新文章《当我谈线程池时我谈些什么——线程池学习笔记》,作为现代C++语法的新学者,想花更多精力在学习现代C++语法的话,适合去看老文章《基于C++11实现线程池》。
言归正传,在那篇老文章里有挺多网友提了一些std::thread
相关的问题。今天看了会cppreference,写了点代码实践了下,集中回答了一些问题,顺便水篇文章算是学习笔记了。
std::thread对象构建的新线程何时开始执行
线程在构造关联的线程对象时立即开始执行,从提供给作为构造函数参数的顶层函数开始。
有几点需要注意:
- 顶层函数的返回值将被忽略,而且若它以抛异常终止,则调用
std::terminate
。在需要获取返回值时,顶层函数可以通过std::promise
或者修改共享变量(可能需要锁机制进行线程同步)。 - 当使用不带参数的默认构造函数
thread()
构造std::thread
对象时,该对象不表示任何线程,也不会有新线程产生。 - 当时用移动构造函数
thread(thread&&other)
构造std::thread
对象时,该对象会被构造为表示曾为other
所表示的执行线程的std::thread
对象。此调用后other
不再表示执行线程。 std::thread
对象不可复制(复制构造函数已被删除)。没有两个std::thread
对象会表示同一执行线程。
std::thread对象构建新线程时可以传入什么东西作为参数
std::thread
常用的构造函数如下:
1 | template< class Function, class... Args > |
其中,f
为任意可调用对象(Callable),args
为任意数目的作为可调用对象f
的参数。
可调用对象(Callable)是C++的一个具名要求,常见的函数、成员函数、仿函数(函数对象)都属于可调用对象。
函数
传入函数的情况最常见也最简单,例如:
1 | void f1(int n) |
仅需将函数名与函数参数分别传入即可。
成员函数
当需要传入类时,有两种情况,第一种是比较复杂的情况,我们需要传入类的成员函数。例如:
1 | class foo |
我们需要以std::thread(&类名::成员函数名, &类实例)
的格式传入新线程。
仿函数(函数对象)
仿函数(或称函数对象)便是传入类的第二种情况,此时该类的工作比较简单(单一,并非指实际工作难度),例如标准库中的std::function
, std::bind
等,又例如第一篇文章中的ThreadWorker
类。成为仿函数的类,一般来说需要**重载函数调用运算符()**。在std::thread
对象构建新线程后,会自动进行INVOKE操作执行传入的可调用对象。INVOKE操作执行对象为仿函数时,会自动调用仿函数重载的函数调用运算符operator()
。例如:
1 | class baz |
需要注意的是,新线程运行的仿函数实际上是传入时指定的仿函数的副本,这同时也就要求该仿函数是可拷贝的。
函数参数传引用
std::thread
对象构造新线程时,会移动或按值复制线程函数的参数。若需要传递引用参数给线程函数,则必须包装它(例如用std::ref
或std::cref
)。例如:
1 | void f2(int& n) |
暂时就总结了这些,后续若有内容再更新。