说明
最近做项目的时候,使用了c++11的多线程,以前做项目都是使用微软提供的一些api,比如临界区、Mutex等来实现多线程,但是比如临界区这玩意只在windows下才有,linux是没有这个概念的,所以为了跨平台,c++11之后,就提供了多线程的支持。
关于多线程最经典的书籍应该算是 C++ currency in action 这本书了,基本上所有多线程的难题都能在该书找到答案,我目前也在看这本书。在该书的第二章完整的说明了如何启动一个线程、执行线程、识别线程、像线程传递参数、以及线程为什么不能被拷贝等。
detach() 注意点
detach()的作用是将子线程和主线程的关联分离,也就是说detach()后子线程在后台独立继续运行,主线程无法再取得子线程的控制权,即使主线程结束,子线程未执行也不会结束。当主线程结束时,由运行时库负责清理与子线程相关的资源,也就是说创建线程后,一般直接调用该线程的detach(),和其他线程分离,一旦主线程(main线程)执行完毕,线程的生命周期也就结束了,不管线程里面的逻辑是不是已经执行完毕,因此如果使用过程中,有涉及从其他线程传递到本线程参数的时候,一般使用值传递,而不使用引用和指针(这是因为可能传递的引用或指针指向的值已经被回收了)
join() 注意点
一般需要等待线程时,我们可以使用join()函数,所以detach()里面关于参数的问题在join()一般是不存在的,这是因为假如我们在一个函数创建了线程,并将该线程使用join()加入到线程的等待中,这时候函数就会等待这个线程执行完毕,不管传递给线程的是局部变量还是其他任何变量,只要变量在该函数中访问,那么其创建的线程也能访问这个变量。一般需要使用join()等待线程时,不会在创建后立即调用join(),因为这样可能会导致创建线程去等待这个线程执行完成才能继续执行;特别创建线程的是主线程的时候,如果子线程需要执行很长时间,那么可能会让主线程处于等待中,那么可能就会导致界面假死的情况(界面编程的时候)。所以选择join()的时间点就显得非常重要。下图是从C++ currency in action 中文版(第一版)的截图,很好的说明了join()加入点可能带来的问题。但是使用vs2017运行下面这段程序也会抛出异常,主要原因是catch 之后调用了join(),然后继续throw,没有进行处理会导致异常的发生,如果将throw注释掉,即使走到了catch里面,执行了“1”处的代码,但是还会继续执行“2”处的代码 (可能是我理解有问题吧!反正我是没有正确运行以下代码,抛出了一段异常,这个在我后面代码的注释有写)
join 和 detach 问题的代码部分
上面说了deatch和join带来的问题,下面就看如何用代码来解决这2个问题吧,首先看看detach()的测试代码,代码有详细的注释,首先是创建detach的函数代码
#include <iostream> #include <thread> #include <Windows.h> #include "TesDetach.h" #include "clogex.h" //在主函数里调用该函数 void testDetach() { int localState = 30;//作为值传递 int localRef = 40;//作为引用传递 TestDetach testDetach(localState,localRef);// std::thread t(testDetach);//使用类对象创建线程 t.detach();//分离线程 Sleep(10);//模拟主线程运行时间(10ms) }//函数执行完毕,main函数执行完毕,线程都结束执行,而不是线程一直执行下去 //打印日志类(这里使用了自定义的一个日志类,在资源代码里有), //日志在当前目录的debug下,打印的日志i可以获取正确的值,j获取的值不正确 void TestDetach::logInfo(int i, int j) { ClogEx::ClogExWrite(__FILE__, __LINE__, "print 值传递 i:%d, 引用传递 j:%d \n", i,j); }
上一段代码是创建线程的代码,下面是用于创建线程的类的代码
#pragma once #include <iostream> /* 测试deteach() ,使用函数对象的方式创建线程 */ class TestDetach { public: void logInfo(int k,int j); int m_i;//从创建线程的地方进行赋值 int &m_j;//从创建线程的地方传递引用 TestDetach(int i,int j) :m_i(i),m_j(j){} //函数调用运算符,如果类重载了函数调用运算符,我们可以像使用函数一样使用该类的对象, //如果不懂的详细信息可以参考c++ primer 重载运算与类型转换的函数调用运算符章节 或者其他资料 void operator() () // { //执行100000次的打印日志功能,每次都打印i和j for (int k = 0; k < 100000; ++k) { logInfo(m_i,m_j); } } }; void testDetach();
调用方式也很简单,直接在主函数里进行调用即可
#include <Windows.h> #include "TesDetach.h" int main() { testDetach(); //测试detach()方法 //testJoin(5); //测试参数:这个值大于10会导致异常发生,小于等于10输出数组的值 }
通过以上的调用,我们看看打印的日志截图如下,通过日志可以看出,子线程执行了10ms,同时通过值传递可以得到正确的值,当时引用传递的值却是错误的。
以上代码主要包含几个C++ 知识点: (1) 值传递和引用传递的区别。(2)变量的生命周期(最重要的一条结论是不应该返回局部变量的引用)。(3)c++11创建多线程的方式。 (4)detach()的用法。(5)函数运算符的使用
下面看看Join()的测试代码,首先是测试类的头文件
#pragma once #ifndef PART1H #define PART1H #include <thread> #include <iostream> #include <exception> #include "Mythread.h" /* 测试join方法的使用, 使用普通函数的方式创建线程 */ class TestJoin { public: static void printHelloWolrd();//线程要执行的逻辑 }; void testJoin(int i); void testThrow(int i);//能够捕获的异常 void testNotThrow();//测试不能捕获的异常(没什么用处) #endif // !1
下面是cpp文件
//#include "Common.h" #include "TestJoin.h" void TestJoin::printHelloWolrd()//新线程要执行的逻辑 { std::cout << "hello world\n"; //其他线程需要执行的代码,这里需要保证自己的逻辑不出异常, //如果出了异常,那肯定也不会调用自身的join了 //所以,必须处理好在线程的异常问题,反过来说,这里出了异常, //如果不去处理,线程调用join和不调用join有什么区别 //反正这里已经导致程序死掉了 } void testThrow(int i)//在调用新线程的join()之前主线程要执行的逻辑 { int a[10] = {1,2,3,4,5,6,7,8,9,10}; try { //这里只是为了触发一个异常,奇怪,如果k> 10 ,竟然没有给我抛异常,竟然不给我抛 //那就手动触发吧,知道的网友说下什么时候才会抛这个异常 if (i > 10) throw std::out_of_range("i > 10 throw out_of_range, is just a test,i > 10 not throw a exception"); std::cout << "print array data:"; for (int k = 0; k < i; k++) { std::cout << a[k] << " "; } std::cout << std::endl; } catch (std::out_of_range e) { throw e; } //其他逻辑代码 } void testNotThrow() { int k = 0; try { int i = 100 / k; //捕获不了,直接出异常,不会进入catch } catch(...) { std::cout << "catch the devide 0 exception"; throw new std::runtime_error("devide 0 "); } } void testJoin(int i)//传递的参数是测试值,在主函数里调用 { //用以记录是否发生异常,很奇怪耶,与我想象的不太一样,以为捕获到异常后, //不会继续执行catch后面的代码(跟java的区别吧,java好像如果不加finally, //catch后面的代码是不会执行的,但是finally里面的代码是会执行的) bool hasException = false; std::thread t(TestJoin::printHelloWolrd);//创建线程并启动线程 try { testThrow(i); //主线程执行逻辑 } catch (std::exception e) { hasException = true; std::cout << "catch exeception:" << e.what(); t.join(); // } std::cout << "\n 不管是否执行了catch,这句代码都会执行...."; if (!hasException) t.join(); }
以下是调用代码
// cPlusPlusCurrencyAnction.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 // #include <Windows.h> #include "TestJoin.h" int main() { testJoin(5); //测试参数:这个值大于10会导致异常发生,小于等于10输出数组的值 }
如果调用testJoin(5)代表打印数组的5个值:结果如下:
如果调用testJoin(12)抛出异常:结果如下:
观察 testJoin(int i)函数使用了2次join(),一次是在catch里,另一次在在最后,如果抛出异常,就在异常里直接调用join,否则就在最后进行调用,这在一般情况下是可以,但是如果是一种无法捕捉的异常,即使出现了异常,也不进catch块,这样做不是万无一失的, try/catch块只能够捕捉轻量级的异常错误,在这里如果在调用testThrow(i)时发生严重的异常错误,那么catch不会被触发捕捉异常,同时造成程序直接从函数调用栈回溯返回,也不会调用到join,也会造成线程资源没被回收,资源泄露。
所以在这里有一个方法是使用创建局部对象,利用函数调用栈的特性,确保对象被销毁时触发析构函数的方法来确保在主线程结束前调用join(),等待回收创建的线程的资源。首先创建一个类,代码如下,这个类只有头文件,cpp文件无任何内容
#pragma once #include <thread> //这个类使用了RAII的思想:构造函数创建资源,析构函数释放资源 class Mythread { private: std::thread &m_t; public : explicit Mythread(std::thread &t) :m_t(t) {}//注意不能使用值传递,thread不允许拷贝 ~Mythread() { if (m_t.joinable()) { m_t.join(); } } //不允许拷贝 Mythread(Mythread const&) = delete; Mythread& operator= (const Mythread& p) = delete; };
然后在testJoin函数里作如下变换
void testJoin(int i)//传递的参数是测试值,在主函数里调用 { //用以记录是否发生异常,很奇怪耶,与我想象的不太一样,以为捕获到异常后, //不会继续执行catch后面的代码(跟java的区别吧,java好像如果不加finally, //catch后面的代码是不会执行的,但是finally里面的代码是会执行的) bool hasException = false; std::thread t(TestJoin::printHelloWolrd);//创建线程并启动线程 //将t引用给Mythread类的一个变量,这样函数执行完毕,thread析构时就会调用join Mythread thread(t); try { testThrow(i); //主线程执行逻辑 } catch (std::exception e) { hasException = true; std::cout << "catch exeception:" << e.what(); //t.join(); //如果了 MyThread类自动处理了join(),这里不需要 } std::cout << "\n 不管是否执行了catch,这句代码都会执行...."; //if (!hasException)//没有发生异常,才能join,因为join只能join一次 //t.join(); }
以上代码参考了该博主的文章c++11中关于std::thread的join的思考(他的代码有2处有点小问题,不影响阅读):在此谢过。
以上代码主要包含几个C++ 知识点: (1) RAII思想。(2)变量的生命周期。(3)c++11创建多线程的方式。 (4)join()的用法.(5).C++11类对象不允许拷贝和赋值的方式
题外话
在很多公司面试的时候,多线程和内存管理的题目是很多面试官最喜欢问的问题,上次我就被一个公司问到:为了防止内存泄漏有哪些方法? 其实RAII的思想就是其中一种(c++中的智能指针),并发的2种途径:多线程并发和多进程并发。这么简单的问题,当时竟然没有打出来,所以自然而然就没有下文了,最近的几个月才把c++ primer这本书看了差不多一遍,学到了很多东西。有需要的同学可以找我要,我也是刚入多线程的坑,每次写博客都感觉能学到一些东西