1. 基本线程管理
1.1 启动线程
通过给 std::thread 的构造函数传递一个可调用对象来启动线程。一旦启动了线程,需要显式决定是要等待它完成(结合),还是让它后台自行运行(分离)。如果在 std::thread 对象被销毁前未作决定,那么该线程将会被终止(std::thread 的构函数调用 std::terminate)。
如果不等待线程完成,那么需确保通过该线程访问的数据是有效的,直到该线程完成为止,否则将会出现未定义的行为。如,当线程函数持有局部变量的指针或引用,且当函数退出时线程尚未完成:
#include <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();
}
首先,使用局部变量 some_local_state 初始化结构体对象 my_func,且成员变量 i 为该局部变量的引用。然后,使用可调用对象 my_func 启动线程 my_thread,并调用 detach 使新线程后台运行。
函数 oops 结束后,局部变量被销毁。而由于新线程 my_thread 处于后台运行,无法确定其运行状态。如果调用重载运算符 () 时循环还没有结束,此时成员变量 i 引用内容已被销毁,将导致未定义行为。
1.2 等待线程完成
将上述程序调用 detach 改为调用 join,将足以确保在函数退出前线程已结束。调用 join 将会清除所有与该线程相关联的存储器,它不再与任何线程相关联。只能对给定线程调用一次 join,此时调用 joinable 将返回 false。
如果要分离线程,通常在线程启动后就可立即调用 detach。如果需等待该线程结束,要确保 join 的正确调用。如,需同时在非异常情况和异常情况下调用 join:
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)的方式,提供一个类,在它的析构函数中调用 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;
};
void f()
{
int some_locate_state = 0;
func my_func(some_locate_state);
std::thread t(my_func);
thread_guard g(t);
do_something_in_current_thread();
}
函数 f 结束后,局部对象会按照构造函数的逆序被销毁,g 首先被销毁并调用析构函数的 join 函数。即便后续函数出现异常,也能保证线程 t 的正确结束。
2. 传递参数给线程
传递参数给可调用对象或函数,就是将额外参数传递给 std::thread 的构造函数。如:
void f(int, char)
std::thread t(f, 3, 'c');
2.1 传递局部变量给线程
void f(int, std::string const&);
void oops(int param)
{
char buffer[1024] = "Hello World!";
std::thread t(f, 3, buffer);
t.detach();
}
局部指针变量 buffer 传递给新线程 t 且函数的 f 参数类型为引用,同样会出现上述指针悬空的问题。此外,函数 oops 可能会在新线程中 buffer 被转换为 std::string 之前退出,同样产生未定义行为。后者的解决办法是显式转化:
std::thread t(f, 3, std::string(buffer));
2.2 传递引用给线程
对应于上述前者问题的解决办法:
std::thread t(f, 3, std::ref(std::string(buffer)));
2.3 传递 unique_ptr 给线程
在 unique_ptr 中,一个对象内的数据被转移至另一对象时,原对象变为空壳。移动构造函数和移动赋值运算符允许一个对象的所有权在 unique_ptr 实例之间进行转移。
3. 转移线程的所有权
一个特定执行线程的所有权可在 std::thread 实例之间移动。如:
void func1();
void func2();
std::thread t1(func1);
std::thread t2 = std::move(t1);
t1 = std::thread(func2);
std::thread t3;
t3 = std::move(t2);
t1 = std::move(t3);
首先,基于 func1 启动新线程 t1。然后当 t2 构建完成后,通过 std::move 显式地将 t1 的所有权转移至 t2。接着基于 func2 再次启动线程 t1,并声明一个不与任何线程关联的 t3。最后,使用两个 std::move 转移线程所有权。
在最后一次 std::move 中,由于 t1 已经与 func2 相关联,会调用 std::terminate 来终止程序,确保与 std::thread 的析构函数保持一致。
4. 在运行时选择线程数量
std::thread::hardware_currency 返回一个给定程序执行时能够真正并发运行的线程数量,并发运行时的线程数为该值与硬件线程数量的较小者。
5. 总结
- 以可调用对象为参数初始化 std::thread 对象即可启动线程。一旦线程启动,必须显式指定是要等待它结束(join),还是让它后台运行(detach)。
- 对于可调用对象的参数,将额外的参数传递给 std::thread 的构造函数即可,传递不同类型的参数将产生不同的行为。
- 线程的所有权可在 std::thread 之间转移。
- 并发程序运行时的线程数量受给定程序执行时能够运行的线程数和硬件线程数的影响。
|