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