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对象被析构也不影响子线程,我们也没有直接的方式与线程通信,也不再能join或detach该线程,此时线程的所有权属于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_guard和funcA都要有,不是说直接在主线程里创建一个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博客
(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; }