前言实际程序运行时每个程序都有一个程序入口线程也不例外使用线程时需要给线程提供一个入口函数线程执行完入口函数时线程将退出。C11中提供了std::thread库本文将从线程的启动、线程等待、线程分离、线程传参、线程识别等几个方面介绍初级线程管理的知识。1 线程启动C11中线程的启动终究是对std::thread的对象进行构造。线程构造的类别如下1.1 线程函数无参数无返回值此类可以说是最简单的线程启动函数不需要传参也不需要返回函数执行结果执行完成后线程自动退出。形如12voidFunDoingNothing();std::thread(FunDoingNothing)编写代码时需要加上thread头文件以方便编译器能够正确处理thread对象。1.2 线程函数有参数无返回值C11中thread的构造函数中使用了可变参数这样可以使得构造thread对象时可以自定义传入参数构造函数的定义如下1templateclassF,class... Argsexplicitthread(F f, Args... args);在实际使用时线程函数有参数时可以定义形式如下1234voidprintMsg(inta,intb) {cout input params are: a ,b endl;}std::threadmy_thread(printMsg, 3, 4)1.3 调用可调用的类型构造使用时可以将带有执行函数的变量传入thread的构造函数中从而替换默认的构造函数如下12345678910111213usingnamespacestd;classBackGroundTask{public:voidoperator()()const{doSomeThing();}priavte:doSomeThing();};intmain(){BackGroundTask f;std::threadmyThread(f);}上面的代码中在启动线程时同构构造对象ff对象的重载函数中调用了线程运行时要执行的方法。但有一点需要注意的是在传入临时的构造对象时不经过处理可能会让编译器产生错误的理解。如1std::threadmyThread(BackGroundTask());这里相当与声明了一个名为myTread的函数 这个函数带有一个参数(函数指针指向没有参数并返回BackGroundTask对象的函数) 返回一个std::thread对象的函数 而非启动了一个线程。如果要解决这个问题只需要如下处理即可12std::threadmyThread((BackGroundTask()));std::threadmyThread{BackGroundTask()};当然也可以使用lamda表达式实现上述功能如下123std::threadmyThread([]{doSomeThing();});2 等待线程C11中确保线程执行完后主线程在退出需要在代码中使用join()函数这样就可以保证变量在线程结束时才会进行销毁。2.1 join等待在实际编程时join函数只是简单的等待或者不等待。在有些场景下就会不使用如果想要进行更加灵活的控制需要使用C11中提供的其他机制这个也会在后面的推文中进行说明。在编程时如果对一个线程使用了join那么在后续的操作中如果使用joinable()执行结果将返回false。既一旦使用了join。线程对象将不能重复使用。如下代码中在线程中使用join等待。1234567891011121314151617classBackGroundTask{public:voidoperator()(){doSomeThing();}private:voiddoSomeThing() {cout线程退出endl;};};intmain(){BackGroundTask f;std::threadmyThread(f);myThread.join();cout退出endl;}上面的代码使用了线程等待可以输出正确的结果如下线程退出退出如果将myThread.join()语句注释再次执行时程序将执行出错因为在子线程还没有结束时主线程已经结束。运行结果如下退出terminate called without an active exception上面的输出具备不确定性代码运行时结果随机。2.2 异常场景的join等待异常场景中如果没有充分考虑join的位置就可能会产生因为异常导致主线程先于子线程退出的情况解决这些问题可以通过下面两种方法进行处理2.2.1 通过异常捕获通过分析代码中的异常场景对异常使用try...catch进行捕获然后在需要线程等待的地方调用join()函数这种方法虽然可以轻易地捕获问题并对问题进行修复但并非是通用法则还需要根据实际情况进行分析。如检查并确认是否线程函数中是否使用了局部变量的引用等其它原因。2.2.2 使用RAII方式进行线程等待RAII可以理解为资源获取既初始化。因为全写为Resource Acquisition Is Initialization。实际使用时通过定义一个类然后在析构函数中使用join函数进行线程等待。这样可以避免场景有遗漏的地方。12345678910111213141516classthread_guard{private:std::thread t;public:explicitthread_guard(std::thread t_):t(t_){}~thread_guard(){if(t.joinable()){t.join();}}thread_guard(thread_guardconst)delete;thread_guard operator(thread_guardconst)delete;};如上通过在将线程对象传入到类thread_guard中如果thread_guard类对象的局部变量被销毁则在析构函数中会将线程托管到原始线程。在thread_guard中使用delete标识禁止生成该类的默认拷贝构造、以及赋值函数。在实际编程时如果不想线程等待可以使用detach方法将线程和主线程进行分离。3 线程分离线程分离使用detach方法使用后将不能在对已分离的线程进行管理但是分离的线程可以真实的在后台进行运行。当线程退出时C会对线程资源进行清理和回收。线程分离通常被用作守护线程或者后台工作线程。使用方法如下1234567intmain(){BackGroundTask f;std::threadmyThread(f);myThread.detach();cout退出endl;}4 向线程传递参数向线程传递参数非常简单在上面的代码中也有提及这里主要说下向线程中传递参数的陷阱。看下面的代码12345678voidf(inti,std::stringconst s);voidoops(intsome_param){charbuffer[1024];sprintf(buffer,%i,some_param);std::threadt(f,3,buffer);t.detach();}上面的代码中buffer是一个局部指针变量使用后可能会导致线程出现未定义的行为因为从char*到string的转换时使用的是隐式转换但是thread在使用时会将变量拷贝到线程私有内存但是并不知道需要将参数进行转换因此复制到私有内存的变量就没有转换成期望的对象。如果要解决这个问题可以在使用时直接将参数类型转换成函数默认的类型在上面的例子中可以做如下操作1std::threadt(f,3,std::string(buffer));但是这样做依然存在问题既线程在复制变量到私有内存时只复制了变量值这样在线程调用后如果继续使用线程函数处理后的变量时可能变量并没有改造依旧是线程调用之前的变量。因此要想在函数传参过程中使得线程拷贝时依旧保持引用可以在线程调用时使用引用方式如1std::threadt(f,3,std::ref(std::string(buffer)));5 线程识别每个线程都有一个线程标识在C11中线程标识通过std::thread::id进行标识std::thread::id可以复用并进行比较如果两个线程的id相等那么它们就是同一个线程或者没有线程如果不等就表示两个是不同的线程或者其中一个线程不存在。