c++多线程总结

可以在创建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子线程出现了异常,thread被销毁,主线程就会被异常所终结,join就没有被调用。(例如线程访问了主线程中已经被销毁的局部变量,这是一个很常见的问题)

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

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

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

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

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

为了保证join被正常调用,可以在创建子线程的函数中,对创建子线程之后,调用join之前的代码加上try-catch语句(这里的函数是指:主线程调用函数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(); //防止do_something_else执行发生异常导致funcA异常退出从而join没被调用
        throw;
    }
    t.join(); // 如果do_something_else没发生异常则正常调用join
}

正常来讲创建线程之后就要立马调用join,但由于种种原因主线程还要做一些别的工作(do_something_else),所以要在调用join之前的这个流程加一个try语句并在catch中调用join是为了防止调用do_something_else过程中发生异常导致主线程挂掉从而没有正常调用join,而第二个join的作用是,如果do_something_else正常执行没发生异常也能在funcA在退出前调用join,也就是保证作为局部变量的thread对象t被析构前调用join(t在funcA退出后被析构,但funcA会永远在子线程终止后才退出,因为如果子线程没终止的话它就被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对象,funcA退出后就会自动调用~std::thread(注意g的析构函数先被调用,t作为成员其析构函数后调用)

需要强调的是,上面的2种方式,都是为了保证线程在第1,2种情况下退出能调用join(至于子线程终止前调用还是终止后调用都无所谓,如果是终止前调用就会阻塞主线程直到子线程终止),如果是thread自身异常退出,这种情况是没办法的,join了也没用(整个线程池都挂了),所以子线程函数自身内部要做好异常防护(例如在线程函数内部自己使用try...catch来捕获异常) c++之多线程中“锁”(mutex)的用法_c++ mutex_one-莫烦的博客-CSDN博客  (同样对锁使用了RAII)

 

还有一个值得注意的问题。关于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后子线程还与主线程共享内存空间吗

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

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

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

主线程还没挂的话子线程是可以访问全局变量的,但挂了之后再访问相当于访问被析构的变量,那就是未定义行为

参考:

C++ 多线程detach()操作的坑以及传参_c++ detach_土豆西瓜大芝麻的博客-CSDN博客

C++多线程detach()后为什么能够访问已经析构的变量? - 知乎

(20条消息) C++的并发世界(一)——线程管理_c++守护线程_光电的一只菜鸡的博客-CSDN博客

(20条消息) C++-线程的join和detach_detach和join_mrbone11的博客-CSDN博客

关于c ++:主线程退出后线程访问共享变量 | 码农家园

子线程如何访问主线程的全局变量 -CSDN社区 

(31条消息) C++ 多线程编程(一):std::thread的使用_std thread_N阶魔方的博客-CSDN博客  (有些描述的不是很清楚,在上面做了澄清)

 

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

 

 

#include <iostream>
#include <thread>

using namespace std;

class A {
public:
    int ai;
    A (int i) : ai(i) 
{ 
    cout << "构造" << this << endl; 
}

A (const A& a) :ai(a.ai) {
    cout << "拷贝构造" << this << endl;
}

~A()
{
    cout << "析构" << this << endl;
}
};

//void test(const A a)
void test(const A a)
{
    cout << "子线程开始" << endl;
    cout << "子线程结束" << endl;
    return;
}


int main()
{
    cout << "主线程开始" << endl;
    int i = 4;
    thread t(test, A(i)); // thread的构造函数构造t时拷贝一次A,创建线程时再拷贝一次A,把A传入test时又拷贝一次,所以一共调用A 1次构造函数,3次拷贝构造函数
    t.detach();
    cout << "主线程结束!" << endl;
    return 0;
}

 

posted @ 2023-05-21 21:00  大黑耗  阅读(749)  评论(0编辑  收藏  举报