c++多线程总结

发布时间 2023-05-21 21:04:56作者: 大黑耗

可以在创建thread的实例后立即调用detach,这样主线程就会与thread的实例分离,即使出现了异常thread的实例被销毁,仍然能保证主线程在后台运行。通常称分离线程为守护线程(daemon threads),UNIX中守护线程是指,且没有任何用户接口, 并在后台运行的线程。这种线程的特点就是长时间运行;线程的生命周期可能会从某一个应 用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进 行优化。另一方面,分离线程的另一方面只能确定线程什么时候结束,"发后即忘"(fire and forget)的任务就使用到线程的这种方式。不能对没有执行线程 的 std::thread 对象使用detach(),也是join()的使用条件,并且要用同样的方式进行检查—— 当 std::thread 对象使用t.joinable()返回的是true,就可以使用detach()

thread对象是用来管理这个子线程的,std::thread 的析构函数会调用 std::terminate()来终止子线程,所以如果thread对象被析构,子线程也会终止。但是一旦thread对象调用了detach线程与thread对象将不再有关联,thread对象被析构也不影响子线程我们也没有直接的方式与线程通信,也不再能joindetach该线程,此时线程的所有权属于C++运行时库,它保证在线程退出时相关资源被回收。分离的线程通常称为守护进程,它们通常在程序的整个生命周期运行,做一些监控、清理工作。同样的thread对象只能被detach一次

有时候:1. 主线程想知道子线程是否已经结束(可以设置超时,需要比如条件变量和期待(futures)这些来完成)2. 主线程需要得到子线程的结果后才能继续往下执行;3.由主线程负责回收子线程的资源,这时候就需要使用join等待子线程结束(虽然调用join会阻塞主线程,但是调用join还是宜早不宜迟,因为怕在调用join之前子线程就先退出变成僵尸进程,变成僵尸线程后再调用join也无法回收其资源?),但线程以join方式运行时,需要在主线程的合适位置调用join方法,如果调用join子线程出现了异常,thread被销毁,主线程就会被异常所终结,join就没有被调用。(例如线程访问了主线程中已经被销毁的局部变量,这是一个很常见的问题)

注意,调用join并不是为了防止子线程异常导致主线程退出,子线程一旦发生异常整个线程池都会挂掉!这跟是否调用join没关系,调用join只是为了让主线程等待回收子线程的资源

子线程在3种情况下会退出:

1. 正常执行完指令正常退出;

2. 子线程还在活动,但thread对象被析构(没有调用detach),子线程被~std::thread()调用std::terminate()强行退出;

3. 子线程发生异常退出。

为了保证join在子线程退出前被调用,且无论是否发生异常它都被调用,可以在创建子线程的函数中加上try-catch语句,并且通常来说,我们希望即使发生异常也调用join方法等待。(这里的函数是指:主线程调用函数funcA,这个函数funcA里会创建线程thread t(tfunc); 前面说的函数就是这个函数funcA,而不是线程跑的函数tfunc)。

例如下面的funcA

void funcA() {
thread t([]{
cout << "hello C++ 11" << endl;
});
 
try
{
do_something_else(); // join之前主线程先去干点别的
}
catch (...)
{
t.join();
throw;
}
t.join();
}

 

加一个try语句并在其中调用join是为了防止子线程异常导致主线程也挂掉从而没有正常调用join,而第二个join的作用是,直接把join的调用放到这个funcA里,就保证了funcA在退出前就调用了join,也就是保证作为局部变量的thread对象t被析构前调用join

 

另一种方式:资源获取即初始化(RAII,Resource Acquisition Is Initialization)

class thread_guard

{

thread &t;

public :

explicit thread_guard(thread& _t) :

t(_t){}

 

~thread_guard()

{

if (t.joinable())

t.join();

}

 

thread_guard(const thread_guard&) = delete;

thread_guard& operator=(const thread_guard&) = delete;

};

 

void funcA(){

 

thread t([]{

cout << "Hello thread" <<endl ;

});

 

thread_guard g(t);

}

 

跟前面的join同样道理,主线程调用funcA函数,函数退出后会析构g,析构函数会调用join来阻塞等待线程t终止(注意使用这种方式来保证调用join的话thread_guardfuncA都要有,不是说直接在主线程里创建一个thread_guard对象,而是要在主线程里调用funcA,在funcA里创建thread_guard对象,当然有一点要注意,要保证在创建的子线程t终止前退出funcA,也就是在t终止前调用join,所以最好是把thread_guard g(t);放在函数最后面,这样声明之后就可以立马退出funcA从而调用~std::thread(注意g的析构函数先被调用,t作为成员其析构函数后调用)

 

还有一个值得注意的问题。关于C++语法解析的问题:“C++s most vexing parse”。

class background_task

{

public:

 void operator()() const

 {

 do_something();

 do_something_else();

 }

};

background_task f;

std::thread my_thread(f);

 

这里我们构造了一个函数对象f,并传入thread的构造函数中。如果我们直接这么写

std::thread my_thread(background_task());

我们本意是希望使用background_task的构造函数返回一个background对象,并直接用这个临时对象构造一个thread对象。但是在C++中,上面的表达式会被解析为:声明了一个my_thread函数,返回值为thread对象,参数为一个函数指针,这个函数指针指向的函数没有参数,且其返回值background_task对象。

为了避免这种歧义发生,除了提前声明一个函数对象之外,还可以:

1. 新增一对括号
std::thread my_thread((background_task()));

2. 使用初始化语法

std::thread my_thread{background_task()};

 

 

使用C++线程库启 动线程,可以归结为构造 std::thread 对象。如果 std::thread 对象销毁之前还没有做出决定,程序就会终止 ( std::thread 的析构函数会调用 std::terminate() )。因此,即便是有异常存在,也需要确保线程能够正确的加入(joined)或分离(detached)。如果不等待线程,就必须保证线程结束之前,可访问的数据得有效性。这不是一个新问题 ——单线程代码中,对象销毁之后再去访问,也会产生未定义行为——不过,线程的生命周 期增加了这个问题发生的几率。 这种情况很可能发生在线程还没结束,函数已经退出的时候,这时线程函数还持有函数局部变量的指针或引用。

 

 

调用detach后子线程还与主线程共享内存空间吗

不共享了,因为主线程退出了,子线程也不会退,说明他两不在一个空间内(?)

还是说,依然是共享的,只不过主线程退出后由于这个被detach出来的子线程还没结束,所以它的内存空间仍然会保留(但是每个线程的堆栈是独立的)?但是主线程里的变量依然都会被析构销毁掉

那也就是说调用detach后子线程不能在访问一些主线程的全局变量了?

 

 

c++多线程如何防止内存泄漏?