02.线程管理

C++标准库只管理与std::thread关联的线程。

2.1 线程的基本操作

每个程序至少有一个执行main()函数的线程,其他线程与主线程同时进行。如同main()函数执行完会退出一样,线程执行完函数也会退出。

2.1.1 启动线程

简单来说,使用C++线程库启动线程,就是构造std::thread对象,例如:

void do_some_work();
std::thread my_thread(do_some_work);//普通函数的函数名就是函数的地址,此行代码将函数的地址传递给线程

需要包含<thread>头文件,std::thread可以和任意可调用类型(callable type)一起发挥作用,所以std::thread可以通过有函数操作符类型的实例进行构造:

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());

上述代码相当于声明了一个名为my_thread的函数,这个函数带有一个参数(函数指针指向没有参数并返回background_task对象的函数),返回一个std::thread对象的函数。
而没有创建一个新的线程。
为了避免这种情况的发生,可以使用额外的括号或者统一的新的初始化语法。例如:

std::thread my_thread((background_task()));
std::thread my_thread{background_task()};

还有一种方式是使用Lambda表达式。例如:

std::thread my_thread([]{
do_something();
do_something_else();
});

一旦开始了线程,就需要显式的决定是等待线程结束还是让它自己运行。如果在std::thread对象销毁之前没有决定,程序就会终止(std::thread的析构函数就会调用std::terminate())。因此,即便有异常存在,也要线程能够正常汇入(joined)或分离(detached)。
例如在函数已经退出后,这时线程函数还持有函数局部变量的指针或引用,例如下列的代码就说明了这个问题:

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();//不等待线程结束,新线程可能还在运行
}

上述代码,使用了detach()就是不再等待线程。不应该使用访问局部变量的函数去创建线程。

2.1.2 等待线程完成

如果需要等待线程,需要使用join()。调用join(),还可以清理线程相关的内存,这样std::thread对象将不再与已经完成的线程有任何关联。即,只能对一个线程使用一次join(),std::thread对象就不能再次汇入,当对其使用joinable()时,将返回false。

2.1.3 特殊情形下的等待

如果需要分离线程,可以在线程启动后,直接使用detach()分离。如果等待线程,需要挑选使用join()的位置。因为,在线程运行后产生的异常,会在join()调用之前抛出,这样就会跳过join()。

避免异常被抛出的异常终止,通常,在无异常的情况下使用join()时,需要在异常处理过程中调用join(),从而避免生命周期的问题。例如下列的代码:

struct func{
int& i;
func(int& i_) : i(i_){}
void operator() (){
for(unsigned j = 0;j <1000000;++j){
do_something(i);
}
}
};
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();//*
}

上述代码中,使用try/catch块保证线程退出后函数才结束。当函数退出后,程序会执行到*处。当执行过程中抛出异常,程序会执行到**处。如果线程在函数之前结束,就需要查看是否因为函数使用了局部变量的引用,而后再确定一下程序可能会退出的途径。使用RAII(Resource Acquisition Is Initialization),提供一个类,在类的析构函数中使用join()。例如下列代码:

class thread_guard{
std::thread& t;
public:
explicit thread_guard(std::thread& t_):t(t_) {}//构造函数
~thread_guard(){
if(t.joinable()){
t.join();//2
}
}
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);
}
}
};
void f(){
int some_local_state = 0;
func my_func(some_local_state);
std::thread t(my_func);
thread_guard g(t);
do_something_in_current_thread();
}

在void()函数执行完毕后,局部对象就会被逆序销毁,也就是首先销毁thread_guard类的对象g,这时线程在析构函数中被加入2到原始线程中。即使do_something_in_current_thread()抛出一个异常,这个销毁依旧会发生。

拷贝构造函数和拷贝赋值操作标记为= delete,是为了不让编译器自动生成。直接对对象进行拷贝或赋值是很危险的,因为这可能会弄丢已经汇入的线程。通过= delete操作,任何尝试给thread_guard对象赋值的操作都会引发一个编译错误。

2.1.4 后台运行线程

使用detach()会让线程在后台运行,也就是线程不能与主线程直接交互。如果线程分离,就不可能有std::thread对象能引用它,

posted @   yyyyyllll  阅读(11)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示