跳至主要內容

多线程 - 线程参数与数据

LincZero大约 6 分钟

多线程 - 线程参数与数据

线程传参

临时对象(值传递)

引用传递(错误写法)

#include <iostream>
#include <thread>			// 线程头文件

using namespace std;

void myprint(const int &i, char *pmybuf)	// 带参数的线程的初始函数
{
    cout << i << endl;						// 输出:1 				【调试】&i = 0x0072d9e
    cout << pmybuf << endl;					// 输出:this is a test!	【调试】&pmybuff = 0x004ffb64
}
    
int main()
{
    int mvar = 1;							// 【调试】&mvar = 0x0022fa58 {1}
    int &mvary = mvar;						// 【调试】&mvary = 0x0022fa58 {1}
    char mybuf[] = "this is a test!";		// 【调试】&mybuff = 0x004ffb64
    thread mythread(myprint, mvar, mybuf);	// 注意这里传参会有点不一样
    mythread.detach();
    cout << "主线程" << endl;
    return 0;
}
  • 存在问题

    • i,看似引用传参,但实际上是值传递,指向的不是同一地址!
    • pmybuf,是真正的引用传参
    • 不推荐用引用、detach子线程中绝对不可以用指针
  • 解决方案

    • 简单类型传值,传字符串时参数为const string &pmybuf,打印pmybuf.c_str()
    • 这里要加const的原因:略

隐式转换传递(错误写法)

#include <iostream>
#include <thread>			// 线程头文件
using namespace std;

void myprint(const int i, const string &pmybuf)	// 带参数的线程的初始函数
{
    cout << i << endl;						// 输出:1 				【调试】&i = 0x0072d9e
    cout << pmybuf.c_str() << endl;			// 输出:this is a test!	【调试】&pmybuff = 不同
}

int main(){
    int mvar = 1;							// 【调试】&mvar = 0x0022fa58 {1}
    int &mvary = mvar;						// 【调试】&mvary = 0x0022fa58 {1}
    char mybuf[] = "this is a test!";		// 【调试】&mybuff = 0x004ffb64
    thread mythread(myprint, mvar, mybuf);	// 注意这里传参会有点不一样
    mythread.detach();
    cout << "主线程" << endl;
    return 0;
}
  • 存在问题

    • mybuf什么时候转换成string?有可能主线程结束后,mybuf被回收后才转换成string,此时会出问题
  • 解决方案

    • 直接转换,不要隐式转换。因为隐式转换的过程会在子线程中

显示类型转换为临时变量(正确写法)

#include <iostream>
#include <thread>			// 线程头文件
using namespace std;

void myprint(const int i, const string &pmybuf)
{
    cout << i << endl;
    cout << pmybuf.c_str() << endl;					// 【调试】00C0D298
}

int main(){
    int mvar = 1;
    int &mvary = mvar;
    char mybuf[] = "this is a test!";
    thread mythread(myprint, mvar, string(mybuf));	// 直接转换,不要隐式转换	【调试】0079F6D0
    mythread.detach();
    cout << "主线程" << endl;
    return 0;
}
  • 隐式转换和显式转换区别(可以用线程id来调试一下并验证)
    • 隐式转换
      • string类的活动:一次类型转换构造函数(传参时 子线程)、一次析构函数(子线程)
    • 显示转换
      • string类的活动:进行一次类型转换构造函数(主线程)、一次复制构造函数(传参时 主线程)、两次析构函数(一次主线程、一次子线程)
    • 区别原因
      • 显示转换写法中,能确保类型转换后立刻调用复制构造函数。因为这里的复制构造函数不是传参引发的,而是Thread构造函数内部的一个行为
      • 但隐式转换写法中,类型转换构造函数不是Thread完成的,和普通函数一样不能确保传参后立刻调用
  • 结论
    • 传递int这种简单类型参数,值传递

    • 不建议使用detach

类对象(ref函数传递)

拷贝构造写法(地址不同)

#include <iostream>#include <thread>			// 线程头文件
using namespace std;void myprint(const int i, const string &pmybuf){
    cout << "《子线程》" << endl
        <<"《线程id》"<<std::this_thread:get_id()<<endl;
}

class A						// 要传递的类对象
{
public:
    mutable int m_i;
    A(int a):m_i(a){
        cout<<"【构造函数】"<<endl
            <<"【地址】"<<this<<endl
            <<"【线程id】"<<std::this_thread:get_id()<<endl;
    }
    A(const A &a):m_i{
        cout<<"【复制构造函数】"<<endl
            <<"【地址】"<<this<<endl
            <<"【线程id】"<<std::this_thread:get_id()<<endl;
    }
    ~A(){
        cout<<"【析构函数】"<<endl
            <<"【地址】"<<this<<endl 
            <<"【线程id】"<<std::this_thread:get_id()<<endl;
    }
}

int main(){
    A aObj(10);		// 类对象
    thread mythread(myprint, aObj);				// 直接传
    mythread.join();
    cout << "《主线程》" << endl
        <<"《线程id》"<<std::this_thread:get_id()<<endl;
    return 0;
}
  • 调试结果
    • 类对象一次构造函数(主线程)、一次拷贝构造函数(子线程)、两次析构函数(主线程、子线程各一次),两次构造的对象地址不同
    • 这种写法可以用detach

ref写法(地址相同)

int main(){
    A aObj(10);		// 类对象
    thread mythread(myprint, std::ref(aObj));	// 【修改】ref,真正的引用传递,令得主线程和子线程公用同一个地址的同一个对象
    mythread.join();
    cout << "《主线程》" << endl
        <<"《线程id》"<<std::this_thread:get_id()<<endl;
    return 0;
}
  • 调试结果

    • 主线程和子线程公用同一个地址的同一个对象
    • 类对象一次构造函数(主线程)、一次析构情况(主线程)
    • 这种写法不能用detach,会出错
  • 深层原理

    • 传递类对象时,避免隐式类型转换。创建线程时构建临时对象,并用引用来接

      线程初始函数用引用来接,这里的引用并没有失效。如果用值传递的话,会有三次构造函数!一次默认构造、两次复制构造

      这里其中的一次复制构造函数不是传参造成的,而是新建线程时的默认行为:thread构造函数内部会有一个构造tuple

  • 选用

    • 类对象全部使用ref()就挺好的

智能指针(move函数传递)

错误写法

int main(){
    unique_ptr<int> ap(new int(100));			// 智能指针
    thread mythread(myprint, ap);				// 直接传,会报错。报错原因:unique不支持拷贝构造函数
    mythread.join();
    cout << "《主线程》" << endl
        <<"《线程id》"<<std::this_thread:get_id()<<endl;
    return 0;
}
  • 报错原因
    • unique不支持拷贝构造函数

移动语义方案

int main(){    
    unique_ptr<int> ap(new int(100));			// 智能指针
    thread mythread(myprint, std::move(ap));	// 【修改】移动语义方案(本质是所有权转移)
    mythread.join();
    cout << "《主线程》" << endl
        <<"《线程id》"<<std::this_thread:get_id()<<endl;
    return 0;
}
  • 调试结果
    • 主线程和子线程的智能指针指向同一块地址
    • 这种写法不能用detach,会出错
  • 危险性
    • 线程都有独立堆栈空间,主线程的智能指针指向主线程堆栈空间,在std::move后主线程智能指针为空
    • 但通过new分配的内存依然在主线程中,子线程指针指向的内存在主线程中
    • 当主线程结束后,内存回收,子线程智能指针指向的是主线程地址,会导致程序错乱
    • 总结:也就是说当主线程和子线程存在共享内存数据时,绝对不能用detach()

共享参数

只读数据

直接使用即可,加上const更安全

#include <iostream>
#include <thread>			// 线程头文件
using namespace std;
vector <int> g_v = {1,2,3};	// 【共享数据】不传参也可被线程直接使用

void myprint(const int i){
    cout << "《子线程》" << endl
        <<"《线程id》"<<std::this_thread:get_id()<<endl;
    cout << g_v[0] << endl;	// 【共享数据】直接使用
}

int main(){
    vector <thread> mythreads;
    for(int i=0; i<10; i++)
    {
        mythreads.push_back(thread(myprint, i));
    }
    for(auto iter=mythreads.begin(); iter!=mythreads.end(); iter++)
    {
        iter->join();
    }
    cout << "《主线程》" << endl 
        <<"《线程id》"<<std::this_thread:get_id()<<endl;
    return 0;
}

可读可写(互斥量加锁)

这种情况下,代码写得有问题会很容易导致程序崩溃

  • 关键处理:读的时候不能写、写的时候不能读/写
  • 深层原因:写的操作不原子性,可能正在写的时候任务切换,发生问题
  • 解决方案:用互斥量加线程锁,将共享数据的操作锁住

应用举例

  • 比如火车有10个售票窗口(或者电影院网上订座),当订座时不能冲突