C++11并发与多线程

0 引言 

并发多线程是目的与手段之间的关系。并发是指大于等于2的活动同时发生。并发对软件行业的影响见下文。

https://blog.csdn.net/hsutter/article/details/1435298

(1)线程:线程是用来执行代码的一条通路,一个新的线程代表一条新的通路。

(2)进程:运行起来的可执行程序,每个可执行文件运行起来,在操作系统的任务管理器中显示为一条进程。线程与进程的关系:每个进程都有一个唯一的主线程。

(3)主线程:用来执行main函数的线程叫做主线程,主线程与进程唇齿相依。

(4)软件并发:软件并发是指多个进程通过任务的快速切换,从呈现效果上来说展示为并发的一种状态。

(5)硬件并发:指多核处理器可以真的同时执行多个任务。

(6)并发的实现方式有两种:一种是多进程并发,一种是多线程并发。多进程并发要考虑通信。进程之间的通信方式有很多种,同一台电脑上分为:管道,文件,消息队列,共享内存等;不同电脑上可以采用socket通信技术。进程之间存在数据保护,进程之间的通信是一个复杂的事情。

(7)多线程并发:一个进程中的所有线程共享地址空间,通过全局变量等在线程之间传递,其开销远远小于多进程。因此,一般并发优先考虑使用多线程技术。

1 多线程编程

(1)自己创建的函数要从一个初始函数开始执行。

(2)join:加入和汇合的意思,一个thread对象执行join的效果是让主线程等待子线程执行完毕,再执行主线程。这也是传统的多线程程序推荐的方法,这样写出来的程序可以保证每个子线程都有有头有尾。

(3)detach与join是相对的,是一个特例,一个thread对象执行了detach命令后,主线程与子线程分离,主线程不必等待子线程执行完毕再执行,主线程也不必非得等待子线程结束。detach之后,子线程被c++运行时库接管了。detach会使子线程失去控制,使用有风险。一旦detach了,不能再join了,否则系统会报告异常。

(4)joinable: 判断线程对象是否可以join或者detach,一般可以join就可以detach,不能join就不能detach,所以没有detachable.

2 子线程初始函数中的临时对象传递问题 

(1)普通函数中的临时对象传递问题

首先创建测试用例如下。
    void MyPrint(const int & temp_i, char* temp_ch) {
        std::cout <<temp_i<< std::endl;
        std::cout << ch << std::endl;
    }

    void TestMyPrint() {
        int i = 10;
        int &ref_i = i;
        char ch[] = "This is a test!";
        MyPrint(ref_i , ch);
    }
在运行此用例时,利用vs中的快速监视功能,观察变量的值,发现:
&i =
0x0021f9c0 {10}
&ref_i = 0x0021f9c0 {10}
&temp_i = 0x0021f9c0 {10}
可以得出结论:普通函数中,当形参为引用时,操作系统不会复制此变量。
ch = 0x002ff838 "This is a test!"
temp_ch = 0x002ff838 "This is a test!"
可以得出结论:普通函数中,当形参为指针时,操作系统不会复制此变量。
因此,采用引用和指针传递变量的效率会比创建其他变量要高,因为操作系统需要额外复制一份临时变量。

(2)子线程的初始函数中的临时对象传递问题

    void MyPrint(const int & temp_i, char* temp_ch) {
        std::cout << temp_i << std::endl;
        std::cout << temp_ch << std::endl;
    }
  void TestTempObj()
    {
        int i = 10;
        int &ref_i = i;
        char ch[] = "This is a test!";
        std::thread temp_thread(MyPrint, ref_i, ch);
        temp_thread.join();
    }

在运行此用例时,利用vs中的快速监视功能,观察变量的值,发现:
&i = 0x0021f9b4 {10}
&ref_i = 0x0021f9b4 {10}
&temp_i = 0x002ab44c {10}
可以得出结论,主线程在创建子线程时,其初始函数中的引用符号失效,引用变量仍然会单独复制一份。
ch = 0x0021f990 "This is a test!"
temp_ch = 0x0021f990 "This is a test!"
可以得出结论,
主线程在创建子线程时,其初始函数中的指针变量的值不变,不会单独复制一份。
由于这个原因,在创建子线程时需要特别小心传递指针类型的变量,很容易出错。另外,传递引用类型变量时,如果发生了类型转换,也有可能会出错。

 (3)结论

在子线程的初始函数中,建议采用如下规范:

(a)若传递int这种简单类型的变量,建议采用值传递,降低风险;

(b)若传递的是类对象,避免隐式类型转换,因为存在如下情况:

class A{
   int m_a;
   A(int a):m_a(a){
   
   }
   ~A();     
};

void Print(const A& a){
     
}
int main()
{
int a_int = 10; std::thread my_thread(Print, a_int); // 此处的a_int可以隐式转化为A my_thread.detach();
return 0;
}

在这种情况下,my_thread子线程与主线程分离,存在一种可能:即主线程已经执行完了,a_int对象已经被内存给释放了,但是子线程初始函数中的构造函数还未执行,使得该类型转换无法完成,程序出错。

(c)子线程的初始函数传递的参数类型为类对象时,必须使用引用,如果不用的话,函数将调用三次拷贝构造函数,造成严重的浪费。

(d)建议在操作子线程时,不使用detach,只是用join, 这样就不存在局部变量时效,导致对内存的非法引用问题。

(4)临时对象构造时机的捕捉

    TA::TA(const int& i) : m_i(i) {
        std::cout << "TA(const int& i) 构造函数执行" << "线程的id号是: " << std::this_thread::get_id() << std::endl;
    }

    TA::TA(const TA& a) {
        this->m_i = a.m_i;
        std::cout << "TA(const TA& a) 拷贝构造函数执行" << "线程的id号是: " << std::this_thread::get_id() << std::endl;
    }

    TA::~TA() {
        std::cout << "~TA()析构函数执行" << "线程的id号是: " << std::this_thread::get_id() << std::endl;
    }

 (5)通过std::ref修饰子线程的初始函数中的参数对象,使得该值可以被修改;此时,编译器不再强行复制该引用值,而是直接采用引用的方式传递值,函数将可以在子线程中修改参数,并传回到主线程中。举例:

    void ChangeValue(const TA& a) {
        a.m_i = 18993838;
    }

    void TestMyPrint1() {
        std::cout << "主线程的id号是: " << std::this_thread::get_id() << std::endl;
        int i = 19;
        TA a(19);
        std::thread temp_thread(ChangeValue, std::ref(a));
        std::cout << a.m_i << std::endl;
        temp_thread.join();
    }

 

posted @ 2020-03-22 16:05  十步一杀2017  阅读(528)  评论(0编辑  收藏  举报