目录
1. 基本线程管理
1.1 启动线程:
1. 2 线程等待:
detach(): 不等待线程任务结束
join(): 等待线程任务结束
2. 线程函数参数传递
3. 线程所有权的转移
4. 运行时可用线程数量
5. 线程标识
当线程thread对象被构造后,开始执行该对象被指定的任务。thread构造方式:
//指定运行的函数(void func(void))
void do_some_work();
std::thread my_thread(do_some_work);
//可调用callable类
class backgroud_task
{
public:
void operator () () const
{
do_some_work();
}
};
background_task f;
std::thread my_thread(f);
std::thread my_thread( (backgroud_task()) );
std::thread my_thread{ backgroud_task() };
注意:当直接传递一个临时的且未命名的变量时,编译器会将其解释为函数声明,而非thread对象构造,因此要极其注意上述的括号,额外的括号避免其解释为函数声明。
std::thread my_thread([]{
do_something();
});
线程开启后,需显式地决定是否要等待任务完成,如果在thread对象被销毁前未做决定,那么将会调用thread的析构函数terminate()来终止程序。
将通过以下方式来确定是否等待任务完成:
void oops ()
{
int state=0;
func my_func(state);
std::thread my_thread(my_func); //线程执行的任务my_func需要依赖oops作用域内的局部变量
my_thread.detach();
}
当oops退出时,my_thread相关联的线程并不受影响,仍在运行;
my_thread调用detch()会把线程丢在后台运行,也没有直接的方法与之通信,这样分离的线程的所有权和控制权被转交给c++运行时库,以确保与线程相关联的资源在线程退出后能被正确回收。分离的线程通常被称为守护线程。
注意:当该线程需要访问oops执行期间创建的对象时,因为其已经被销毁,将会出错,因此当决定不等待线程任务结束时,需要使线程函数自包含需要使用的数据(数据copy而不是数据共享);
当调用thread.join()时,需等该线程结束后,才会继续剩余的code, 而在该线程结束前,程序将会阻塞等待(不占用cpu)。因此,需要谨慎选择调用的join的位置,防止在调用join之前发生了异常而实际没有调用到join, 以确保访问局部状态的线程在函数退出前结束。
为确保线程在函数退出前完成,可使用以下方式:
try/catch块:
void oops()
{
int state=0;
func my_func(state);
std::thread my_thread(my_func);
try
{
...
my_func.join();
...
}
catch(...)
{
my_func.join();
throw;
}
}
注意: 使用try/catch块容易将作用域弄乱,所以并不是一个很合理的方案,更推荐第二种方式;
资源获取即初始化(RAII):
在其析构函数中进行join();
class thread_guard
{
std::thread& t;
public:
//explicit thread_guard (std::thread& t_): t(t_) {}
explicit thread_guard (std::thread& t_): t(std::move(t_)) {}
~thread_guard()
{
if (t.joinable())
t.join();
}
/* 构造函数及拷贝赋值运算符被标记为delete,以确保他们不会由编译器自动提供 */
thread_guard(thread_guard const &)=delete;
thread_guard& operator=(thread_guard const &)=delete;
}
void oops ()
{
int state=0;
func my_func(state);
//std::thread t(my_func);
//thread_guard g(t);
thread_guard g(std::thread(func))
...
}
注意:在析构函数中调用join前首先要测试下该thread是不是joinable的,因为一个给定的执行线程join只能被调用一次;
传递参数给可调用对象或函数时,基本上就是将额外的参数传递给std::thread构造函数,参数传递时,参数会被简单粗暴地复制到内部存储空间,在那里新创建的执行线程在访问时,这些被复制进内部空间的参数再转换成期望的参数类型。
参数被复制到内部存储空间时:
a. 参数为指针时,直接将指针copy到内部空间,而指针指向的对象并不会被复制,因此,若在执行线程使用该参数前,释放该指针指向的对象,将会出现未定义行为。
void f(int i, std::string const s);
//std::thread t(f, 3, "hello");
std::thread t(f, 3, std::string("hello")); //避免悬浮指针
直接使用"hello"传参时,将会以char const*传送,并在内部空间转换为std::string.
b. 参数为对象或对象的引用时,直接将整个对象copy到内部空间。但是若为引用时,在执行线程要执行时,再将在内部存储空间的副本对象的引用传递给执行线程,是无法实现引用的效果的。
可用std::ref来包装需要引用的参数,用于显性指示对象的引用,如:
void update_data_for_widget(widget_id w, widget_data & data);
std::thread t(update_data_for_widget, w, std::ref(data));
c. 线程执行特定对象的成员函数,如,
class X
{
public:
void do_lengthy_work();
};
X my_x;
//线程调用的是my_x.do_lengthy_work()
std::thread t(&X::do_lengthy_work, &my_x);
d. 当传入的参数不能被复制只能被移动时,可以通过std::move来将源对象的所有权先被转移到新创建的线程的内部存储中,然后,再在执行的时候传入。
std::thread的实例是可移动的,但不是可复制的。线程的所有权的移动也是通过std::move完成的。当thread对象A将所有权转交给一个已经有关联的线程B时,线程B会调用terminate()来终止程序。
可以利用哪些移动感知的容器来生成一批线程,然后等待他们完成,如,std::vector
std::thread::hardware_currency() 返回一个对于给定程序执行时能够真正并发运行的线程数量的指示,注意,返回的线程数已包含当前线程,所以,在构建最大线程数时,需要减一。若该信息不可用,则返回为0.
线程标识符为std::thread::id类型,线程库为该类型的对象提供了完整的比较运算符,及排序方法。因此,thread::id可以在关系型容器中被用作主键或被排序。
std::thread::id可通过直接调用thread对象的get_id()来获取,或者通过调用std::this_thread::get_id()来获取。