C++并发编程实战笔记 [02] :线程管控
发起线程
线程通过构建 std::thread
对象而启动,该对象指明线程要运行的任务。可以传入任何可调类型给 std::thread
来构建一个 std::thread
对象。 需要包含头文件 <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);
此时要防范二义性,对于有可能被解释成函数声明的C++语句,编译器就肯定会将其解释为函数声明。如 std::thread my_thread(background_task());
语句本意是传入一个临时的匿名函数对象发起新线程,但却会被解释成一个函数声明。可以使用统一初始化语法来解决:
std::thread my_thread((background_task()));
std::thread my_thread{background_task()};
- 传入的可调类型可以是lambda表达式
std::thread my_thread([]{
do_something();
do_something_else();
});
必须时刻注意在线程运行结束前,要保证它所访问的外部数据必须始终正确、有效。尤其要注意传给线程的可调对象含有指针或引用时,引用的外部对象在线程运行期间是否可能会被销毁,是否可能出现悬空指针。
等待线程完成
std::thread my_thread(foo);
my_thread.join(); //等待线程结束
对于某个给定的线程,join()
只能调用一次,只要 std::thread
对象曾经调用过 join()
,线程就不再可汇合,成员函数 joinable()
将返回 false
。
利用 RAII 过程等待线程完成
如果打算等待线程结束,但在调用 join()
前就发生了异常,这会导致 join()
调用会被略过。为了在可能出现异常的情况下等待线程完成,最好是利用RAII过程。
RAII(Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。
class thread_guard {
std::thread& t;
public:
explicit thread_guard(std::thread& t_) :t(t_) {}
~thread_guard() {
if (t.joinable()) t.join(); // join() 只能被调用一次
}
thread_guard(thread_guard const&) = delete; // 禁止拷贝构造函数
thread_guard& operator=(thread_guard const&) = delete; // 禁止赋值构造函数
};
struct func {
int& i;
func(int& i_) :i(i_) {}
void operator()() {
for (unsigned j = 0; j < 1000000; ++j)
do_something(i); // i是引用,需要注意可能导致悬空引用
}
};
void f() {
int some_local_state = 0;
func my_func(some_local_state);
std::thread t(my_func);
thread_guard(t);
do_something_in_current_thread();
}
当主线程执行到 f()
末尾时,按构建的逆序,所有局部对象都会被销毁,thread_guard
的对象 g
首先被销毁,在其析构函数中调用新线程的 join()
。即使 do_something_in_current_thread()
发生异常,g
的析构函数仍会被调用,以上行为仍会发生。
分离线程
std::thread my_thread(foo);
my_thread.detach(); //分离线程
只有 joinable()
返回 true
,线程才可被分离。分离后 joinable()
将返回 false
。
向线程函数传递参数
直接向 std::thread
的构造函数追加更多参数即可:
#include <iostream>
#include <thread>
void add(int a, int b) {
std::cout << a + b << std::endl;
}
int main() {
std::thread t(add, 2, 3); // 输出5
t.join();
return 0;
}
创建一个 std::thread
对象的时候,参数传递分为两步,先传给std::thread,再传给函数。线程具有内部存储空间,参数会按照默认方式先复制到该处,新创建的线程才能直接访问。然后,这些副本被当成临时变量,以右值形式传给新线程上的函数或可调用对象。
在传递引用时,需要使用 std::ref
。
void func(int& a) { a = 233; }
void f() {
int a = 114514;
std::thread t(func, std::ref(a));。// 传递引用
// std::thread t(func, a); 这样将无法通过编译,因为无法将一个右值传递给期望左值引用参数的函数
t.join();
std::cout << a << std::endl; // 输出233
}
将某个类的成员函数设为线程函数,应传入一个函数指针,指向该成员函数,还需要给出对象指针,作为该函数的第一个参数:
class X {
public:
void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work, &my_x);
向线程转移动态对象的归属权:
void process_big_object(std::unique_ptr<big_object>);
std::unique_ptr<big_object> p(new big_object);
p->prepare_data(42);
std::thread t(process_big_object, std::move(p));
使用 std::move
,p所指向的对象的所属权会先转移到新线程的内部存储空间,再转移给 process_big_object()
函数。
移交线程归属权
void some_function();
void some_other_function();
std::thread t1(some_function);
std::thread t2 = std::move(t1);
t1 = std::thread(some_other_function);
std::thread t3;
t3 = std::move(t2); // 此时t3与运行some_function()的线程相关联
t1 = std::move(t3); // t1已有关联的线程,该赋值操作会导致终止整个程序
std::thread
支持移动操作,可以方便地从函数内部返回 std::thread
对象:
std::thread f() {
void some_function();
return std::thread(some_function);
}
std::thread g() {
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));
}
于是,我们可以改进之前的 thread_guard
类,直接将新线程的归属权转移给类,不再需要先创建单独的具名变量,然后传引用。
在运行时选择线程数量
std::thread::hardware_concurrency()
返回程序在各次运行中可真正并发的线程数量。
识别线程
线程ID的类型是 std::thread::id
,可以在与线程关联的 std::thread
对象上调用成员函数 get_id()
来获取,若当前对象没有关联到任何执行线程,则会返回一个值为 0 的默认构造的 std::thread::id
对象。当前线程的ID可以调用 std::this_thread::get_id()
来获得:
void f() {
std::cout << std::this_thread::get_id() << std::endl;
}
void g() {
std::thread t(f);
std::cout << t.get_id() << std::endl;
t.join();
std::thread t2;
std::cout << t2.get_id() << std::endl; // 输出0
}
std::thread::id
可以随意进行复制操作和比较操作。