<thread>
头文件的作用
<thread>
是C++11新引入标准库基础设施,提供对多线程操作的支持。
我们可以用 std::thread
来控制线程的创建、运行、回收。
学习 std::thread
的用法是了解C++多线程编程的第一步。
构造std::thread
对象
- 方法一:传入函数对象
1 | class background_task { |
在这种情况下,函数对象先被 copy
到 std::thread
对象的内部,然后再传参、被调用
- 方法二:传入lambda表达式(也是callable对象)
1 | std::thread my_thread([]{ |
- 方法三:传入函数指针和参数
1 | void f(int i); |
- 方法四:传入对象的成员函数
1 | class X { |
std::thread
成员函数
.join()
作用:
- 等待线程执行完毕
- 清除对象内部与具体线程相关的内存,当前对象将不再和任何线程相关联
- 只能调用一次
.join()
,调用后.joinable()
将永远返回false
例子:1
2
3if (t.joinable()) {
t.join();
}
.detach()
作用:
- 把线程放在后台运行,线程的所有权和控制权交给
C++ Runtime Library
- 当前对象将不再和任何线程相关联
- 调用后
.joinable()
将永远返回false
thread function传参可能遇到的问题
问题一:传入临时的callable对象,编译器会误以为是函数声明
1 | // Wrong |
解决方案:用圆括号或者花括号加以说明
1 | // Correct |
问题二:因为传指针or局部变量的引用,导致thread function可能访问已经被销毁的内容
解决方案:
- 把需要使用的临时变量
copy
到std::thread
内部,不要和局部上下文共享临时变量 - 使用
RAII
(资源获取即初始化)
1 | class thread_guard { |
问题三:因为传指针or局部变量的引用,导致在thread function入参时因强制类型转换而访问已经被销毁的内容
理解这个问题之前,需要先梳理一下 std::thread
对象创建后发生了什么:
- 原线程:调用
std::thread
的构造函数or拷贝赋值运算符 - 原线程:一个callable对象和它的参数被拷贝到新创建的
std::thread
内部 - 新线程:之前被拷贝的一系列参数,现在被传入callable对象(发生强制类型转换)
- 新线程:调用callable对象
- ……
其中,第3步发生在新线程内,我们只知道它发生在第2步之后,却不知道具体的发生时间。
如果第3步发生时,原线程已经退出了相关上下文,那么新线程在传参时,可能对已经被销毁的内容进行类型转换操作。
1 | void f(int i, const std::string &s); |
解决方案:由程序员显式完成传参操作,避免出现类型转换
1 | void not_oops(int some_param) { |
问题四:callable对象的参数要求传引用,但实际传入的是内部拷贝对象的引用(无法对原来的对象进行修改)
1 | void update_data_for_widget(widget_id w, widget_data &data); |
解决方案:使用 std::ref()
声明传引用
1 | std::thread t(update_data_for_widget, w, std::ref(data)); |
使用上的技巧
Trick 1:传入只可move不可copy的对象
在这种情况下,如果原对象是无名的临时对象,那么 move
操作是自动完成的。
如果原对象是命名对象(左值引用),那就需要用 std::move()
来将它转换成(右值引用),之后的的拷贝就自动是 move
完成的。
1 | void process_big_object(std::unique_ptr<big_object>); |
Trick 2:std::thread
的move操作
1 | void some_function(); |
上述程序的最后一行中,对象t1已经与一个正在运行的线程互相绑定,不能接受move
的对象,因此整个程序会调用 std::terminate()
退出。
不能 move
给已经绑定了线程的对象。
Trick 3:在函数传参和返回时使用转移std::thread
的所有权
1 | std::thread f() { |
Trick 4:运行时确定新开线程的个数
1 | const unsigned long hardware_threads = std::thread::hardware_concurrency(); |
Trick 5:获取线程id
线程id有个单独定义的类型,std::thread::id
,该类对象有如下性质:
- 调用
std::thread
对象的get_id()
方法可以得到一个std::thread::id
类型对象 std::thread::id
支持大小比较和相等判断,- 相等即为同一线程
- 若a<b,b<c,那么a<c
- 如果当前对象不与任何正在运行的线程绑定,那么
get_id()
返回一个默认构造的std::thread::id
对象 get_id()
可以被std::cout
打印出来,但它的值没有任何具体意义,标准库也不对它的具体实现类型作保证
参考来源
- C++ Concurrency in Action, 2nd Edition