多线程学习(二)

此篇一共讲了四个大问题:

一.传递临时对象作为线程参数

总结:

a)若传递int这种简单的类型,建议都是值传递,不建议用引用,防止节外生枝
b)如果传递类对象避免隐式类型转换,全部都在创建线程这一行构造出临时对象,然后在函数参数里用引用进行接收,不使用值接收,否在系统还会多构造出一个对象(一共三个对象,浪费
终极结论
c)建议不使用detach(),使用join(),这样就不存在局部变量失效导致线程对内存的非法引用问题

#include "stdafx.h"
#include <iostream>

#include <thread>

using namespace std;

void myPrint(const int& i,char* pBuf)
{
    cout << "子线程中引用变量i的地址:" << &i << endl;                //引用变量i的地址
    cout << "子线程中指针变量pBuf的地址:" << (void*)pBuf << endl;   //字符指针pBuf指向的内存地址

    cout << i << endl;                                               //i不是val的引用,实际为值传递
    
    cout << pBuf << endl;

    return;
}
int main()
{
    int val = 1;                        //整形变量val
    int& val_ = val;                    //对val的引用val_
    char buf[] = "this is a test";      //字符数组

    cout << "主线程中变量val的内存地址:" << &val << endl;      //变量val的地址
    cout << "主线程中引用变量val_的内存地址:" << &val_ << endl;//引用变量val_的地址

    cout << "主线程中字符数组buf的地址:" << &buf << endl;   //字符数组地址即为字符数组第一个元素的地址
    
    thread myThread(myPrint,val_,buf); //创建线程,参数:第一个为调用对象(myPrint函数),后面两个为该函数的参数
    myThread.join();                   //阻塞主线程,等待子线程执行完毕
    //myThread.detach();                  //主线程与子线程分离,两个分别执行

    cout << "主线程执行..." << endl;

    return 0;
}

//要避免的陷阱(存在于detach()情况下,但下面测试使用join(),保证主线程和子线程完整执行,既可以观察到完整输出结果)**
//(1)疑问:使用detach()时,如果主线程先执行完毕,变量val内存回收,则myPrint中的参数i(引用类型)是否可以获取到正确的值?
//解决:分别查看实参val_的内存地址和i的地址,如果相同,则主线程执行完后,val内存回收,则i也不会获取到正确的值
// 如果两者地址不同,说明子线程中该变量是一份拷贝,主线程执行完后,子线程可以正常执行
//test:测试结果如下图所示,子线程中引用变量i的地址与主线程中val的内存地址不同,说明创建线程时,其内部做了复制,不是真引用
//故此种情况下,子线程的引用变量i可以获取到正确的值,虽然可以正确执行,但是不建议这样做。
//(2)疑问:对于指针变量pBuf,会是怎样的情况呢?
//解决:用同样的办法对比内存地址
//test:测试结果如下图所示,buf地址和pBuf地址相同,说明此种情况下,子线程的指针变量pBuf不能获取到正确的值
//故此种情况下,子线程不能正确执行,说明detach()子线程时,绝对不可以传指针

执行结果

//(3)疑问:那么问题来了,怎么可以将字符串安全的传递到子线程当中呢?????
//有一种方法:可以将函数myPrint的第二个参数改为string& pBuf,更改后的myPrint函数和执行结果如下:

void myPrint(const int i,const string& pBuf)
{
    cout << "子线程中引用变量i的地址:" << &i << endl;                //引用变量i的地址
    cout << "子线程中pBuf的地址:" << &pBuf << endl;   //字符指针pBuf指向的内存地址

    cout << i << endl;                                               //i不是val的引用,实际为值传递

    cout << pBuf.c_str() << endl;

    return;
}

从下图中的执行结果可以看出来,两者的地址并不相同,那么这种办法是不是真的安全呢???

执行结果

//(4)疑问:是什么时候将字符数组隐式的转化为string呢,事实上存在buf已经回收(main函数执行完毕),系统才开始转换buf为string这种可能性,也就是说还是存在bug
//解决办法为:直接将buf转换为string临时对象保证在线程中肯定有效
//修改后main函数如下:

int main()
{
    int val = 1;                        //整形变量val
    int& val_ = val;                    //对val的引用val_
    char buf[] = "this is a test";      //字符数组

    cout << "主线程中变量val的内存地址:" << &val << endl;      //变量val的地址
    cout << "主线程中引用变量val_的内存地址:" << &val_ << endl;//引用变量val_的地址

    cout << "主线程中字符数组buf的地址:" << &buf << endl; //字符数组地址即为字符数组第一个元素的地址

    thread myThread(myPrint, val_, string(buf)); //创建线程,参数:第一个为调用对象(myPrint函数)
                           //后面两个为该函数的参数,将buf直接转换为string类型,创建string临时对象
    myThread.join();                   //阻塞主线程,等待子线程执行完毕
    //myThread.detach();                  //主线程与子线程分离,两个分别执行

    cout << "主线程执行..." << endl;

    return 0;
}

//但这样就没问题了吗???万一还没有将buf转为string对象,main函数就执行完毕了呢
//下面对此问题进行测试
****** 未使用临时构造对象 ***********

class A
{
public:
    int m_i;
    A(int a) :m_i(a)
    {
        cout << "A的有参构造" << endl;
    }

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

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

void myPrint(const int i, const A& pBuf)
{
    cout << &pBuf << endl;      //pBuf对象的地址
    return;
}

int main(int argc, char** argv)
{
    int val = 1;
    int val_A = 12;

    thread myThread(myPrint, val, val_A);   //val_A可以隐式转换为自定义的A类型,传递给myPrint的第二个参数
    myThread.detach();                    //主线程与子线程分离

    return 0;
}

下面是为对子线程detach()后,发现子线程还未执行完,主线程已经退出,可以推出:主线程的变量val_A已经回收无法隐式转换为A类型,会出现此bug(未定义行为)

执行结果

****** 使用临时构造对象 ***********
//修改main函数如下:

int main(int argc, char** argv)
{
    int val = 1;
    int val_A = 12;

    thread myThread(myPrint, val, A(val_A));//为第二个参数构造临时对象,确保在main执行完成之前,将myPrint需要的参数构造好
                                  
                                            //在创建线程的同时构造临时对象的方法传递参数是可行的

    myThread.detach();                    //主线程与子线程分离

    return 0;
}

可以看到输出中有有参构造、拷贝构造以及对应的析构,分析:在构建临时对象时调用了有参构造,而将此临时对象传递给函数时,thread内部并不是真正处理为引用(myPrint的第二个参数为引用类型),与第一个提出的问题一样,而是将此临时对象进行copy传递给myPrint**。
结论:只要用临时构造的A类对象作为参数传递给线程,那么就一定能够在主线程执行完之前,把线程函数的第二个参数构造出来,确保即使detach()后,子线程也能安全运行

image.png

//又有个问题,在myPrint中 为什么不使用值传递接收临时对象呢???
// 测试一下
//修改myPrint函数如下:

void myPrint(const int i, const A pBuf)  //使用值传递接收临时对象
{
    cout << &pBuf << endl;      //pBuf对象的地址
    return;
}

执行结果如下图所示,会发现多了一次拷贝构造,造成资源浪费,故传递对象时,不建议使用值传递

执行结果

二.临时对象作为线程参数继续

2.1线程ID:每个线程(主线程和子线程)都对应一个ID(数字),不同的线程对应的ID自然也不同,线程ID可以用std::this_thread::get_id()来获取;
2.2临时对象构造时机捕获

class A
{
public:
    int m_i;
    A(int a) :m_i(a)
    {
        cout << "A的有参构造:" << endl;
        cout << "thread ID=" << std::this_thread::get_id() << endl;  //获取线程ID
    }

    A(const A& a) :m_i(a.m_i)
    {
        cout << "A的拷贝构造:" << endl;
        cout << "thread ID=" << std::this_thread::get_id() << endl;  //获取线程ID

    }

    ~A()
    {
        cout << "A的析构" << endl;
        cout << "thread ID=" << std::this_thread::get_id() << endl;  //获取线程ID

    }

};
void myPrint(const A& pBuf)   //引用传递
{
    cout << "子线程myPrint:" << endl;
    cout << "thread ID=" << std::this_thread::get_id() << endl;
}
int main(int argc, char** argv)
{
    int val_A = 1;

    cout << "主线程ID=" << std::this_thread::get_id() << endl;

    thread myThread(myPrint, val_A);         //隐式类型转换
    //thread myThread(myPrint, A(val_A));    //构造临时对象

    myThread.detach();
    //myThread.join();

    return 0;
}

//根据输出结果发现,发生隐式类型转换时是在子线程中构造的A类对象,这就是问题的本质(如果主线程执行完毕,val_A被回收,则无法在子线程中构造A类对象

执行结果

//再一次测试在创建线程的时候构造临时对象
修改main函数为:

int main(int argc, char** argv)
{
    int val_A = 1;

    cout << "主线程ID=" << std::this_thread::get_id() << endl;

    //thread myThread(myPrint, val_A);
    thread myThread(myPrint, A(val_A));

    //myThread.detach();
    myThread.join();

    return 0;
}

根据下面的执行结果可以得到:构造(有参构造和拷贝构造)都是在主线程main中执行的,故可以解决上述问题,即main函数结束之前一定会构造临时对象

image.png

//另一个测试为,当线程函数传递类对象时,不传引用,而传值时,是什么结果,
修改myPrint函数如下:

void myPrint(const A pBuf) //值传递
{
    cout << "子线程myPrint:" << endl;
    cout << "thread ID=" << std::this_thread::get_id() << endl;
}

根据下面的执行结果发现,值传递会进行三次构造,两次在主线程中,一次在子线程中不建议这样使用。

执行结果

三:传递类对象、智能指针作为线程参数

示例代码如下

class A
{
public:
    mutable int m_i;              //m_i可以修改
    A(int a) :m_i(a)
    {
        cout << "A的有参构造:" << endl;
        cout << "thread ID=" << std::this_thread::get_id() << endl;  //获取线程ID
    }
    A(const A& a) :m_i(a.m_i)
    {
        cout << "A的拷贝构造:" << endl;
        cout << "thread ID=" << std::this_thread::get_id() << endl;  //获取线程ID
    }
    ~A()
    {
        cout << "A的析构" << endl;
        cout << "thread ID=" << std::this_thread::get_id() << endl;  //获取线程ID
    }
};
void myPrint(const A& pBuf)         //使用const防止编译器报错(不使用的话老版本的编译器可能会报错,新版本编译器可能会报错)
{
    pBuf.m_i = 199;                                                  //修改该值不会影响到main函数
    cout << "子线程myPrint:" << endl;
    cout << "thread ID=" << std::this_thread::get_id() << endl;
}
int main(int argc, char** argv)
{
    A obj(10);                    //生成一个类对象
    thread myThread(myPrint, obj);//将类对象obj作为线程参数

    myThread.join();

    cout << obj.m_i << endl;     //仍然为10

    return 0;
}

从下面的执行结果可以看出来,虽然我们在myPrint函数中传递的是引用,但是根据上面测试的种种结果来看,此处不是真正的引用,而是值传递,故在子线程中修改对象的属性值,并不会影响到主线程中传入的对象的值

执行结果

//问题:怎么让其成为真正的引用呢?即在子线程中修改对象的属性值后,主线程也随之修改
//解决:std::ref 函数
//测试:修改后的代码如下

class A
{
public:
    int m_i;              //去除mutable
    A(int a) :m_i(a)
    {
        cout << "A的有参构造:" << endl;
        cout << "thread ID=" << std::this_thread::get_id() << endl;  //获取线程ID
    }

    A(const A& a) :m_i(a.m_i)
    {
        cout << "A的拷贝构造:" << endl;
        cout << "thread ID=" << std::this_thread::get_id() << endl;  //获取线程ID

    }

    ~A()
    {
        cout << "A的析构" << endl;
        cout << "thread ID=" << std::this_thread::get_id() << endl;  //获取线程ID

    }

};
void myPrint(const A& pBuf)         //使用const防止编译器报错(不使用的话老版本的编译器可能会报错,新版本编译器可能会报错)
{
    pBuf.m_i = 199;                                                  //修改值
    cout << "子线程myPrint:" << endl;
    cout << "thread ID=" << std::this_thread::get_id() << endl;
}
int main(int argc, char** argv)
{
    A obj(10);                    //生成一个类对象
    thread myThread(myPrint, std::ref(obj));//将类对象obj作为线程参数,并使用std::ref函数传入真正的引用

    myThread.join();

    cout << obj.m_i << endl;      //该值为在子线程中修改后的值

    return 0;
}

根据如下运行结果可以看出,传递到子线程中的引用变量地址与主线程中的对象地址相同,且子线程中的对象的属性值修改后,主线程中的也随之改变,故实现了真正意义上的引用传递

执行结果

智能指针作为线程参数

void myPrint(unique_ptr<int> pzn)        
 //使用const防止编译器报错(不使用的话老版本的编译器可能会报错,新版本编译器可能会报错)
{
    cout << "myPrint线程" << endl;
    cout << &pzn << endl;
}
int main(int argc, char** argv)
{
    unique_ptr<int> myPtr(new int(100));    //独占式指针

    thread myThread(myPrint, std::move(myPtr));
    //使用std::move将myPtr指向的内容转到myPrint的参数pzn中,则myPtr就为空了
    //如果不使用std::move,会出错,因为独占式指针不允许其他指针与自身同时指向同一块内存

    myThread.join();
    //myThread.detach();// 不可以使用detach,因为主线程如果先执行完,
    //则myPtr指向的内存被回收,而子线程中pzn也指向这块被回收的内存,这样是有问题的

    return 0;
}

自行测试如果不使用std::move,编译器报错情况。并使用编译器Debug功能测试子线程中智能指针与myPtr是否指向的是同一块内存

四:用成员函数指针作为线程函数

class A
{
public:
    int m_i;              //m_i可以修改
    A(int a) :m_i(a)
    {
        cout << "A的有参构造:" << endl;
        cout << "thread ID=" << std::this_thread::get_id() << endl;  //获取线程ID
    }

    A(const A& a) :m_i(a.m_i)
    {
        cout << "A的拷贝构造:" << endl;
        cout << "thread ID=" << std::this_thread::get_id() << endl;  //获取线程ID

    }

    ~A()
    {
        cout << "A的析构" << endl;
        cout << "thread ID=" << std::this_thread::get_id() << endl;  //获取线程ID

    }
    void thread_work(int num)   //成员函数做线程函数
    {
        cout << "子线程thread_work" << endl;
    }
    void operator()(int num)           //重载()操作符,作为线程函数
    {
        cout << "子线程()" << endl;
    }
};

int main(int argc, char** argv)
{
    A obj(10);        //生成一个A类对象

    thread myThread1(&A::thread_work, obj, 15);            //创建线程1(使用成员函数thread_work),此种情况下用detach也可以,子线程中为该对象obj的拷贝,会调用拷贝构造
    //thread myThread2(&A::thread_work, std::ref(obj), 15);  //此处测试过程中出错,不能使用std::ref
    thread myThread3(&A::thread_work, &obj, 15);           //创建线程3(使用成员函数thread_work),也是真正的引用,不能使用detach, &obj等价于std::ref(obj),不调用拷贝构造
    thread myThread4(obj,15);                              //创建线程4(使用A类中的重载()的成员函数)
    thread myThread5(std::ref(obj), 15);                   //创建线程5(使用A类中的重载()的成员函数),真正的引用,不能使用detach
    //thread myThread6(&obj, 15);                            //此处测试过程中出错,不能使用&

    //有些可以使用std::ref,而有些不能,有些可以使用&,而有些不能,可以自己尝试

    myThread1.join();  
    //myThread2.join();
    myThread3.join();
    myThread4.join();
    myThread5.join();
    //myThread6.join();

    return 0;
}

具体结果可以自己运行测试。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容