image frame

星际旅行日志

彼汾一曲,言采其藚.彼其之子,美如玉.美如玉,殊异乎公族.

c++11并发与多线程【第三节】线程传参详解

第三节 线程传参详解,detach()大坑,成员函数做线程函数

一、传递临时对象作为线程参数(使用detach()之后容易出现的问题)

1.1 要避免的陷阱一

//此时i的引用并没起作用,不是mvar的引用,实际上是值传递,因此detach之后i的值不会被销毁,是安全的,然而pmybuf的地址仍然和mybuf相同,故不安全
void myprint(const int &i,char *pmybuf){//指针在detach子线程时,绝对会有问题
    cout<<i<<endl;
    cout<<pmybuf<<endl;

}
int main(){
    //传递临时对象作为线程参数
    int mvar=1;
    int &mvary=mvar;
    char mybuf[]="this is a test";
    thread mytobj(myprint,mvar,mybuf);
    mytobj.detach();
    cout<<"I love china1"<<endl;
    return 0;
}

输出:

I love china1
1
QUUUU

此时i的引用并没起作用,不是mvar的引用,实际上是值传递,因此detach之后i的值不会随着主线程中的mvar被销毁,是安全的,然而pmybuf的地址仍然和mybuf相同,故不安全。指针在detach子线程时,绝对会有问题。

1.2 要避免的陷阱二

//此时理论上来说,pmybuf的地址和mybuf是不同的,char*会被隐式转换为string,类似与值传递;然而,输出结果有问题
void myprint(const int i,const string &pmybuf){
    cout<<i<<endl;
    cout<<pmybuf.c_str()<<endl;
}
int main(){
    //传递临时对象作为线程参数
    int mvar=1;
    int &mvary=mvar;
    char mybuf[]="this is a test";
    thread mytobj(myprint,mvar,mybuf);//问题在于mybuf是什么时候被转换成string的
    mytobj.detach();
    cout<<"I love china1"<<endl;
    return 0;
}

此时理论上来说,pmybuf的地址和mybuf是不同的,char*会被隐式转换为string,类似与值传递;然而,输出结果有问题。

I love china1
1
RUUUU

问题在于mybuf是什么时候被转换成string的?
从结果来看,会出现mybuf在main函数执行完成后,即主线程完成后,系统才用mybuf去转换为子线程中的string。

如果改成如下写法:

thread mytobj(myprint,mvar,string(mybuf));//直接将mybuf转换成string对象,生成一个临时的string对象

此时可以保证子线程中的值不会出错。这里使用的是string引用,也即是pmybuf和string(mybuf)这个临时对象是同一个地址。那么这个临时对象的创建时间一定会在主线程结束之前吗?

void myprint(const int i,const string &pmybuf){//这里使用的是string引用,也即是pmybuf和string(mybuf)这个临时对象是同一个地址

需要验证这个问题:
首先验证不使用临时对象的情况:

//用于测试构造函数执行时间是否在主线程完成之前
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 &pmybuf){
    cout<<&pmybuf<<endl;//打印pmybuf对象的地址
}
int main(){
    int mvar=1;
    int mytest=12;
    thread mytobj(myprint,mvar,mytest);
    //mytobj.join();
    mytobj.detach();
    //cout<<"I love china1"<<endl;
    return 0;
}

此时的输出结果中可能会出现什么都无法输出的情况:,这是由于主线程已经执行完毕,而A的构造函数发生在主线程结束之后,mytest已经被销毁,不能提供有效的整形变量来作为构造函数的参数。

其次,验证使用临时对象A(mytest)来作为子线程参数的情况:

//用于测试构造函数执行时间是否在主线程完成之前
class A{
public:
    int m_i;
    //构造函数
    A(int a):m_i(a){cout<<"A的构造函数执行"<<this<<endl;}
    A(const A &a):m_i(a.m_i){cout<<"A的复制构造函数执行"<<this<<endl;}
    ~A(){cout<<"A的析构函数执行"<<this<<endl;}
};
void myprint(const int i,const A &pmybuf){
    cout<<&pmybuf<<endl;//打印pmybuf对象的地址
    return;
}
int main(){
    int mvar=1;
    int mytest=12;
    thread mytobj(myprint,mvar,A(mytest));
    //mytobj.join();
    mytobj.detach();
    //cout<<"I love china1"<<endl;
    return 0;
}

输出结果:
说明在主线程结束之前,mytest被销毁之前,使用mytest构造的对象就已经传入了子线程。从输出结果来看,主线程结束之前,A(mytest)使用构造函数生成了一个临时对象,然后在传递到子线程的过程中又把这个临时对象复制到了其他两个额外生成的对象当中,故:void myprint(const int i,const A &pmybuf) 参数中虽然使用的是A的引用但是实际上并没有生效,而是又额外创建了一个临时的A对象。故:主线程提前于子线程结束并不会导致子线程中的数据被销毁,因为不是同一个数据了。

(备注:老师的使用的是windows+VS stdio ,我使用的是ubuntu +VS Code,老师的输出结果只有一个复制构造函数和对应的析构函数,而我的输出结果有两个复制构造函数和对应的析构,代码一致,猜测可能是编译器不同导致的,倘若我去掉myprint() 函数定义参数中A类对象的引用符&,则会出现3个复制构造函数)

A的构造函数执行0x7fffffffdb88
A的复制构造函数执行0x7fffffffdb40
A的复制构造函数执行0x55555556d2c8
0x55555556d2c8
A的析构函数执行0x55555556d2c8
A的析构函数执行0x7fffffffdb40
A的析构函数执行0x7fffffffdb88

1.3 结论

:在创建线程的同时,构造临时对象的方法传递参数是可行的。只要用临时构造的A类对象作为参数传递给线程,那么就一定能够在主线程执行完毕前把线程函数的第二个参数传递到子线程当中。**

使用detach()应该注意的情况:
(1)若传递int这种简单的类型参数,建议都使用值传递,不要用引用,防止在不同的编译器中出现其他情况。(原因暂时不知)
(2)如果传递类对象,避免隐式类型转换,全部都在创建线程的参数中使用构造临时对象(原因后续知晓),然后在子线程的初始函数的参数中使用引用,否则会在初始函数这又复制出一个临时类对象,浪费内存资源。
(3)建议一般最好不使用detach(),只使用join(),避免main函数中的局部变量失效导致子线程中的使用出错。

二、传递临时对象作为线程参数

2.1 线程ID概念

每个线程(主线程和子线程)都有一个不同的ID编号。线程ID可以使用C++标准库中的函数来获取:
std::this_thread::get_id()

2.2 再次测试,带线程ID分析

//用于测试构造函数执行时间是否在主线程完成之前
class A{
public:
    int m_i;
    //构造函数
    A(int a):m_i(a){cout<<"A的构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    A(const A& a):m_i(a.m_i){cout<<"A的复制构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    ~A(){cout<<"A的析构函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
};

void myprint(const A &pmybuf){
    cout<<"子线程myprint的参数地址是:"<<&pmybuf<<" 线程ID= "<<std::this_thread::get_id()<<endl;//打印pmybuf对象的地址
    return;
}
int main(){
    //传递临时对象作为线程参数
    cout<<"主线程ID= "<<std::this_thread::get_id()<<endl;
    int mvar=1;
    int mytest=12;
    thread mytobj(myprint,mytest);
    mytobj.join();
    //mytobj.detach();
    //cout<<"I love china1"<<endl;
    return 0;
}

thread mytobj(myprint,mytest);//此时创建子线程直接使用int类型,而myprint()的参数是一个A对象,所以必然会发生隐式类型转换。
输出:

主线程ID= 140737348159296
A的构造函数执行0x7ffff7a4adf4 线程ID= 140737348155136
子线程myprint的参数地址是:0x7ffff7a4adf4 线程ID= 140737348155136
A的析构函数执行0x7ffff7a4adf4 线程ID= 140737348155136

可以看出,使用mytest来创建A的对象发生在子线程当中,留下了后患,倘若使用的是detach(),那么随着mytest在主线程中的销毁,子线程必然可能会出错或得出错误结果。

若在创建子线程是使用临时对象作为参数:

int main(){
    //传递临时对象作为线程参数
    cout<<"主线程ID= "<<std::this_thread::get_id()<<endl;
    int mvar=1;
    int mytest=12;
    thread mytobj(myprint,A(mytest));
    mytobj.join();
    //mytobj.detach();
    //cout<<"I love china1"<<endl;
    return 0;
}

输出:

主线程ID= 140737348159296
A的构造函数执行0x7fffffffdb84 线程ID= 140737348159296
A的复制构造函数执行0x7fffffffdb40 线程ID= 140737348159296
A的复制构造函数执行0x55555556d2c8 线程ID= 140737348159296
子线程myprint的参数地址是:0x55555556d2c8 线程ID= 140737348155136
A的析构函数执行0x55555556d2c8 线程ID= 140737348155136
A的析构函数执行0x7fffffffdb40 线程ID= 140737348159296
A的析构函数执行0x7fffffffdb84 线程ID= 140737348159296

可以看出,此时创建线程的临时对象参数的创建,和临时的复制构造类对象都是在主线程当中,主线程的结束不会影响到子线程。

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

(1)传递类对象:

//用于测试构造函数执行时间是否在主线程完成之前
class A{
public:
    mutable int m_i;//mutable使得m_i在任何情况下都能修改
    //构造函数
    A(int a):m_i(a){cout<<"A的构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    A(const A& a):m_i(a.m_i){cout<<"A的复制构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    ~A(){cout<<"A的析构函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
};

void myprint(const A &pmybuf){
    pmybuf.m_i=199;//修改该值不会影响到main函数
    cout<<"子线程myprint的参数地址是:"<<&pmybuf<<" 线程ID= "<<std::this_thread::get_id()<<endl;//打印pmybuf对象的地址
    return;
}
int main(){
    cout<<"主线程ID= "<<std::this_thread::get_id()<<endl;
    A myobj(10);//生成类对象,使用类对象作为线程参数
    thread mytobj(myprint,myobj);
    mytobj.join();
    //mytobj.detach();
    //cout<<"I love china1"<<endl;
    return 0;
}

输出:

主线程ID= 140737348151104
A的构造函数执行0x7fffffffdb84 线程ID= 140737348151104
A的复制构造函数执行0x7fffffffdb40 线程ID= 140737348151104
A的复制构造函数执行0x55555556d2c8 线程ID= 140737348151104
子线程myprint的参数地址是:0x55555556d2c8 线程ID= 140737348146944
A的析构函数执行0x55555556d2c8 线程ID= 140737348146944
A的析构函数执行0x7fffffffdb40 线程ID= 140737348151104
A的析构函数执行0x7fffffffdb84 线程ID= 140737348151104

考虑以上这种情况,在A类中使成员变量m_i变成可修改的,并且在myprint线程函数中对m_i进行修改,且使用的是引用,然而结果却是只能修改到子线程中的成员变量m_i,不会影响到主线程中的对象参数myobj对象中的m_i。因为编译器出于安全考虑,不管函数参数使用的是否是引用,都统一使用值传递的方式生成一个临时对象。
这显然是不符合预期的,达不到在子线程中 ,修改主线程中对象参数成员变量的目的。如何解决这个问题呢?

解决:使用std::ref()函数: 使用ref告诉编译器,就是要把myobj的引用传递到子线程当中

int main(){
    cout<<"主线程ID= "<<std::this_thread::get_id()<<endl;
    A myobj(10);//生成类对象,使用类对象作为线程参数
    thread mytobj(myprint,std::ref(myobj));//使用ref告诉编译器,就是要把myobj的引用传递到子线程当中
    mytobj.join();
    //mytobj.detach();
    //cout<<"I love china1"<<endl;
    return 0;
}

此时:子线程的初始函数myprint()就可以改成:可以不在参数前使用const;

void myprint(A &pmybuf){
    pmybuf.m_i=199;//修改该值不会影响到main函数
    cout<<"子线程myprint的参数地址是:"<<&pmybuf<<" 线程ID= "<<std::this_thread::get_id()<<endl;//打印pmybuf对象的地址
    return;
}

A类定义成员变量也可以去掉mutable;

class A{
public:
    int m_i;//mutable使得m_i在任何情况下都能修改
    //构造函数
    A(int a):m_i(a){cout<<"A的构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    A(const A& a):m_i(a.m_i){cout<<"A的复制构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    ~A(){cout<<"A的析构函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
};

输出:不会再出现复制构造函数再创建一个临时对象了;且子线程中的对象与主线程中传入的参数地址一致,确实传入了引用;最终主线程中的成员变量成功地被子线程进行了修改;

主线程ID= 140737348151104
A的构造函数执行0x7fffffffdb7c 线程ID= 140737348151104
子线程myprint的参数地址是:0x7fffffffdb7c 线程ID= 140737348146944
主线程中的myobj的m_i=:199
A的析构函数执行0x7fffffffdb7c 线程ID= 140737348151104

(2)传递智能指针:

void myprint(unique_ptr<int> pmybuf){
    cout<<"子线程myprint  "<<" 线程ID= "<<std::this_thread::get_id()<<endl;
    return;
}
int main(){

    cout<<"主线程ID= "<<std::this_thread::get_id()<<endl;
    unique_ptr<int> myp(new int(100));
    thread mytobj(myprint,myp);
    mytobj.join();
    //mytobj.detach();
    //cout<<"I love china1"<<endl;
    return 0;
}

此时编译器会报错:不能转换
在这里插入图片描述解决方法:使用std:move()

int main(){

    cout<<"主线程ID= "<<std::this_thread::get_id()<<endl;
    unique_ptr<int> myp(new int(100));
    thread mytobj(myprint,std::move(myp));
    mytobj.join();
    //mytobj.detach();
    //cout<<"I love china1"<<endl;
    return 0;
}

此时程序中,主线程中的myp的内存地址与子线程中的pmybuf内存地址相同,执行完std:move之后,智能指针所指向的对象移动到子线程的pmybuf中,由于使用的是join(),当子线程执行完毕后,智能指针pmybuf就被释放了。(智能指针这一块我不熟,所以直接复述原话了)

如果使用的是detach(),倘若主线程先于子线程执行完毕,智能指针myp已经被释放了,子线程中的pmybuf就会指向一个不可预知的内存区域,就会出问题。所以不能使用detach()

三.用成员函数指针做线程函数

3.1 使用类中的任意成员函数作为线程函数

class A{
public:
    int m_i;
    //构造函数
    A(int a):m_i(a){cout<<"A的构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    A(const A& a):m_i(a.m_i){cout<<"A的复制构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    ~A(){cout<<"A的析构函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}

    void thread_work(int num){
        cout<<"子线程thread_work执行:"<<this<<"   线程ID="<<std::this_thread::get_id()<<endl;
    }
};

int main(){
    cout<<"主线程ID= "<<std::this_thread::get_id()<<endl;
    A myobj(10);
    thread mytobj(&A::thread_work,myobj,15);
    mytobj.join();
    //mytobj.detach();
    return 0;
}

输出:此时传入子线程的是复制构造的临时对象,可以使用join(),也可以使用detach()

主线程ID= 140737348151104
A的构造函数执行0x7fffffffdb70 线程ID= 140737348151104
A的复制构造函数执行0x7fffffffdb34 线程ID= 140737348151104
A的复制构造函数执行0x55555556d2cc 线程ID= 140737348151104
子线程thread_work执行:0x55555556d2cc 线程ID=140737348146944
A的析构函数执行0x55555556d2cc 线程ID= 140737348146944
A的析构函数执行0x7fffffffdb34 线程ID= 140737348151104
A的析构函数执行0x7fffffffdb70 线程ID= 140737348151104

但是如果 是thread mytobj(&A::thread_work,std::ref(myobj),15);//
或者 thread mytobj(&A::thread_work,&myobj,15);//
则传入子线程中的就是myobj的引用,此时不能使用detach().

3.2 使用重载函数作为线程参数

class A{
public:
    int m_i;//mutable使得m_i在任何情况下都能修改
    //构造函数
    A(int a):m_i(a){cout<<"A的构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    A(const A& a):m_i(a.m_i){cout<<"A的复制构造函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}
    ~A(){cout<<"A的析构函数执行"<<this<<" 线程ID= "<<std::this_thread::get_id()<<endl;}

    void thread_work(int num){
        cout<<"子线程thread_work执行:"<<this<<"   线程ID="<<std::this_thread::get_id()<<endl;
    }
    void operator()(int num)//重载()
    {
        cout<<"子线程()执行:"<<this<<"   线程ID="<<std::this_thread::get_id()<<endl;
    }
};

int main(){
    cout<<"主线程ID= "<<std::this_thread::get_id()<<endl;
    A myobj(10);
    thread mytobj(myobj,15);
    mytobj.join();
    //mytobj.detach();
    return 0;
}

主线程ID= 140737348151104
A的构造函数执行0x7fffffffdb88 线程ID= 140737348151104
A的复制构造函数执行0x7fffffffdb54 线程ID= 140737348151104
A的复制构造函数执行0x55555556d2cc 线程ID= 140737348151104
A的析构函数执行0x7fffffffdb54 线程ID= 140737348151104
子线程()执行:0x55555556d2cc 线程ID=140737348146944
A的析构函数执行0x55555556d2cc 线程ID= 140737348146944
A的析构函数执行0x7fffffffdb88 线程ID= 140737348151104

结论子线程中用到主线程中变量的引用或指针的情况,不能使用detach()

文章内容来源《C++并发与多线程视频课程》

c++11并发与多线程【第四节】创建和等待多个线程

一、创建和等待多个线程

1.1 多个线程的执行顺序

#include<iostream>
#include<vector>
#include<thread>
#include<string>
using namespace std;


//线程入口函数
void myprint(int num){
    cout<<"myprint线程开始执行了,线程编号= "<<num<<endl;
    //
    cout<<"myprint线程结束了,线程编号= "<<num<<endl;
    return;
}
int main(){
    //创建和等待多个线程
    vector<thread> mythreads;
    //创建10个线程,每个线程统一使用myprint作为入口函数
    for(int i=0;i<10;i++)
    {
        mythreads.push_back(thread(myprint,i));//创建10个线程
    }
    for(auto iter=mythreads.begin();iter!=mythreads.end();++iter)
    {
        iter->join();
    }
    cout<<"i love china"<<endl;
    return 0;
}

输出:

myprint线程开始执行了,线程编号= 0
myprint线程结束了,线程编号= 0
myprint线程开始执行了,线程编号= 1
myprint线程结束了,线程编号= 1
myprint线程开始执行了,线程编号= 2
myprint线程结束了,线程编号= 2
myprint线程开始执行了,线程编号= 3
myprint线程结束了,线程编号= 3
myprint线程开始执行了,线程编号= 4
myprint线程结束了,线程编号= 4
myprint线程开始执行了,线程编号= 5
myprint线程结束了,线程编号= 5
myprint线程开始执行了,线程编号= 6
myprint线程结束了,线程编号= 6
myprint线程开始执行了,线程编号= 7
myprint线程结束了,线程编号= 7
myprint线程开始执行了,线程编号= 8
myprint线程结束了,线程编号= 8
myprint线程开始执行了,线程编号= 9
myprint线程结束了,线程编号= 9
i love china

多个线程的执行顺序可能是乱的(说实话,我运行了多次,我的没乱,可能是编译器和操作系统不同导致的),乱,和操作系统内部对线程的调用机制相关。

1.2 主线程等待所有的子线程运行结束,最后主线程结束,使用的是join,使用join更容易写出稳定的程序

1.3 把thread对象放入容器进行管理,便于对一次性创建的大量线程进行管理

二、数据共享问题

2.1 只读的数据

vector<int> g_v={1,2,3};//设置为共享数据

//线程入口函数
void myprint(int num){
    cout<<"线程id 为 "<<std::this_thread::get_id()<<"的线程 打印的g_v值为 "<<g_v[0]<<g_v[1]<<g_v[2]<<endl;
    return;
}
int main(){
    //创建和等待多个线程
    vector<thread> mythreads;
    //创建10个线程,每个线程统一使用myprint作为入口函数
    for(int i=0;i<10;i++)
    {
        mythreads.push_back(thread(myprint,i));//创建10个线程
    }
    for(auto iter=mythreads.begin();iter!=mythreads.end();++iter)
    {
        iter->join();
    }
    cout<<"i love china"<<endl;
    return 0;
}

设置g_v为只读数据,此时程序是安全稳定的。

2.2 有读有写

两个线程写,8个线程读。如果不进行线程对共享数据的同步互斥控制,极有可能会在线程切换的过程中导致各种问题。
案例:
北京—>深圳 火车 T123, 10个售票窗口买票, 1和2窗口同时都要订99号座位。如果1窗口和2窗口同时订,就会产生错误。

即只有实现线程同步,才能保证程序的正确运行。这就涉及达到《操作系统》中的进程和线程管理,同步互斥的概念可以在《操作系统》中进行学习。

三.如何保证安全访问共享数据

3.1 共享数据的保护案例

网络游戏服务器。两个自己创建的线程,一个线程收集玩家命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列当中;而另一个线程从队列当中取出玩家发送来的命令,解析,然后执行玩家需要的动作。
这里的队列可以用vector或者list来实现,二者各有优点;
list:频繁的按顺序插入和删除数据时效率高。
vector: 容器随机插入删除数据时效率高。

该案例使用成员函数作为线程的入口函数;
不加任何保护措施的代码,读写的过程中可能会出错:

#include<iostream>
#include<vector>
#include<thread>
#include<string>
#include<list>
using namespace std;

class A{
public:
    //把收到的消息(玩家命令)放入到一个队列的线程入口函数
    void inMsgRecvQueue(){
        for(int i=0;i<100000;i++){//用数字模拟玩家发送来的命令
            cout<<"inMsgRecvQueue()执行,插入一个元素 "<<i<<endl;
            msgRecvQueue.push_back(i);//把命令放入队列当中
        }
    }

    //从消息队列list中读取玩家命令的线程入口函数
    void outMsgRecvQueue(){
        for(int i=0;i<100000;i++){
            if(!msgRecvQueue.empty()){
                int command=msgRecvQueue.front();//返回第一个元素
                msgRecvQueue.pop_front();//取出后移除该元素
                //然后处理数据
            }
            else
                cout<<"outMsgRecvQueue执行,但是list已经空了 : "<<i<<endl;
        }
        cout<<"end "<<endl;
    }

private:
    std::list<int> msgRecvQueue;//在list中存放玩家发来的命令
};

int main(){
    A myobj;
    std::thread myOutMsgObj(&A::outMsgRecvQueue,&myobj);//第二个参数是引用,作用与std::ref相同,保证是子线程中使用的是主线程中的同一个对象,但是主线程后面必须等待子线程完成
    std::thread myInMsgObj(&A::inMsgRecvQueue,&myobj);
    myInMsgObj.join();
    myOutMsgObj.join();

    cout<<"主线程结束"<<endl;
    return 0;
}

增加保护措施的代码,引入C++解决多线程保护共享数据的概念,“互斥量”,具体请看下一节。

文章内容来源《C++并发与多线程视频课程》

VS Code在ubuntu中执行多线程程序时报错:undefined reference to `pthread_create‘

VS Code在ubuntu中执行多线程程序时报错:

问题描述:

最近在学习C++并发多线程,在ubuntu中使用VS Code编写多线程代码后,Ctrl+F5运行程序,结果出现了以下的问题:

#include<iostream>
#include<vector>
#include<thread>
using namespace std;
//一个单独的main函数,运行实际上是主线程在执行,主线程从main返回,则整个进程执行完毕

//自己创建的线程需要从一个函数开始运行
void MyPrint(){
    cout<<"我的线程开始"<<endl;
    ////////
    cout<<"我的线程结束了"<<endl;
}

int main(){
    //此时这个代码中有两个线程在同时在执行,同时执行两个任务
    thread mytobj(MyPrint);
    mytobj.join();
    cout<<"I love china"<<endl;
    return 0;
}

在这里插入图片描述

> Executing task: C/C++: g++ 生成活动文件 <

正在启动生成...
/usr/bin/g++ -g /home/liukai/文档/Code_WorkSpace/CPP_CONCURRENCY/project.cpp -o /home/liukai/文档/Code_WorkSpace/CPP_CONCURRENCY/project
/usr/bin/ld: /tmp/ccb0x5iT.o: in function `std::thread::thread<void (&)(), , void>(void (&)())':
/usr/include/c++/9/thread:126: undefined reference to `pthread_create'
collect2: error: ld returned 1 exit status

生成已完成,但出现错误.
The terminal process terminated with exit code: -1.

Terminal will be reused by tasks, press any key to close it.

重点在于:

/usr/include/c++/9/thread:126: undefined reference to `pthread_create'
collect2: error: ld returned 1 exit status

原因分析:

百度之后,发现是 在ubuntu平台下调用pthread_create()函数,用gcc编译时出现Undefined reference to ‘pthread_create’的问题。是因为pthread 库不是 Linux 系统默认的库,连接时需要使用静态库 libpthread.a,所以在使用pthread_create()创建线程,以及调用 pthread_atfork()函数建立fork处理程序时,需要链接该库。

解决方案:

知道原因后问题就很好解决了,只需在每次编译时用gcc的-l选项将pthread库链接一下就行了。
在这里插入图片描述
但是能不能在VS Code里使其链接到这个静态库,然后直接Ctrl+F5直接运行,当然可以。
在VS Code的搜索框输入tasks.json,修改tasks.json文件。
在这里插入图片描述

把”args”修改为如下,在最后像命令行中一样加上“-lpthread”,保存后即可ctrl+F5运行多线程程序:

"args": [
    "-g",
    "${file}",
    "-o",
    "${fileDirname}/${fileBasenameNoExtension}",
    "-lpthread"
],

在这里插入图片描述


C++11并发与多线程【第一节】并发,进程,线程的基本概念和综述

一:并发,进程,线程的基本概念和综述

1.1 并发

以往的计算机(单核CPU):某一时刻只能执行一个任务,由操作系统调度,每秒钟执行多次所谓的“任务切换”,造成并发的假象(非真正的并发),这种切换(上下文切换)是要有时间开销的。

并发(Concurrence):两个或多个事件在同一时间间隔内发生。操作系统的并发性指的是计算机系统中同时存在多个运行的程序,因此它具有处理和调度多个程序同时执行的能力。

操作系统中引入程序的目的是使程序能并发执行。

同一时间间隔:并发·

同一时刻:并行。并行性是指系统具有同时进行运算或操作的特性,在同一时刻能完成两种或两种以上的工作。并行性一般需要相关硬件的支持。

多道程序环境下,一段时间内,宏观上有多道程序在同时进行,而在每个时刻,单处理机环境下实际仅能有一道程序执行,因此微观上这些程序仍是分时交替执行的。操作系统的并发性是通过分时得以实现的。

举例:

9:00-9:30吃面,9:30:-9:40上厕所,9:40-10:00看书

则在9:00-10:00这个时间段内,吃面,上厕所,看书这三种行为就是并发执行的。

若在9:00或某个时刻,左手吃面,右手写字,则这两个行为就是并行执行的。

1.2 可执行程序

1.3 进程

程序的一次执行。

1.4 线程

- 每个进程都有一个主线程,这个主线程是唯一的。 进程产生后,这个主线程就随之产生了。
- 程序运行时,实际上时进程的主线程来执行这个main函数中的代码。
- 主线程执行完main函数中return后,表示整个进程运行完毕,主线程也运行完毕。

线程是代码的一条执行通路。

线程不是越多越好,每个线程都需要一个独立的堆栈空间,线程之间的切换要保存很多中间状态,会耗费用于程序运行的时间。

二:并发的实现方法

实现并发的手段:
1.多个进程并发
2.单进程中创建多个线程实现并发

2.1 多进程并发

进程之间的通信(同一电脑):管道,文件,消息队列,共享内存;
(不同电脑):socket通信技术;

2.2 多线程并发

线程是轻量级的线程,每个线程都有自己的运行路径,一个进程中的所有线程共享该进程的地址空间(共享内存)。
因此全局变量,指针,引用都可以在线程之间传递,所以多线程的开销远小于多进程。
共享内存的问题:数据一致性问题;

2.3 总结

和进程相比,线程具有以下优点:
(1)线程启动速度更快,更轻量级;
(2)系统资源开销更少,执行速度更快;

缺点:难度大,数据一致性问题;

三:C++新标准线程库

C++11之前的多线程代码不能跨平台,从C++11开始,C++语言本身增加对多线程的支持,即增加了跨平台的可移植性。

文章内容来源《C++并发与多线程视频课程》

c++11并发与多线程【第二节】创建一个线程

一、线程运行的开始结束范例

程序运行起来后,生成一个进程,该进程所属的主线程自动开始运行;

//一个单独的main函数,运行实际上是主线程在执行,主线程从main返回,则整个进程执行完毕
int main(){
    cout<<"I love china"<<endl;
    return 0;
}

主线程从main()函数开始执行,那么我们自己创建的线程也需要从一个函数开始运行(即初始函数),一旦这个函数运行完毕,就代表着我们的线程运行结束。

整个进程是否执行完毕的标志是:主线程是否执行完。一般情况下,如果主线程执行完毕,但是其他子线程(自己用代码创建的线程)还没执行完毕,那么这些子线程就会被操作系统强行终止。

因此,要保证子线程顺利执行,主线程不能先于子线程执行完毕。

创建一个线程:
a.需要包含头文件 thread.h;
b.编写初始函数;
c.main函数中编写线程代码

//自己创建的线程需要从一个函数开始运行
void MyPrint(){
    cout<<"我的线程开始"<<endl;
    ////////
    cout<<"我的线程结束了"<<endl;
}

int main(){
    //此时这个代码中有两个线程在同时在执行,同时执行两个任务
    thread mytobj(MyPrint);
    mytobj.join();
    cout<<"I love china"<<endl;
    return 0;
}

1.1 thread类:位于C++11之后标准库中的类

  • thread mytobj(MyPrint);//创建了线程,并且以Myprint()为初始函数,然后MyPrint() 线程开始执行;

1.2 join():加入/汇合,实质上就是让主线程阻塞,使得主线程等待子线程执行完毕,然后主线程和子线程汇合

  • mytobj.join(); //主线程在这里阻塞,直到子线程MyPrint 执行完毕
#include<iostream>
#include<vector>
#include<thread>
using namespace std;
//一个单独的main函数,运行实际上是主线程在执行,主线程从main返回,则整个进程执行完毕

//自己创建的线程需要从一个函数开始运行
void MyPrint(){
    cout<<"我的线程开始1"<<endl;
    cout<<"我的线程开始2"<<endl;
    cout<<"我的线程开始3"<<endl;
    cout<<"我的线程开始4"<<endl;
    cout<<"我的线程开始5"<<endl;
    ////////
    cout<<"我的线程结束了"<<endl;
}

int main(){
    //此时这个代码中有两个线程在同时在执行,同时执行两个任务
    thread mytobj(MyPrint);
    mytobj.join();
    //mytobj.detach();
    cout<<"I love china1"<<endl;
    cout<<"I love china2"<<endl;
    cout<<"I love china3"<<endl;
    cout<<"I love china4"<<endl;
    return 0;
}

在这里插入图片描述

1.3 detach() :通常主线程需要在子线程完成后再退出,detach()是个例外

detach:分离,主线程不和子线程汇合了,二者不必再互相等待运行结束。
一旦detach之后,与主线程关联的的thread线程对象,就会失去与主线程的关联,各走各的路,此时这个子线程就会驻留在后台运行(主线程与子线程失去联系),这个子线程相当于被C++运行时库接管,此时该子线程叫做守护线程;当这个子线程执行完成后,有运行时库负责清理该线程相关的资源。

int main(){
    //此时这个代码中有两个线程在同时在执行,同时执行两个任务
    thread mytobj(MyPrint);
    //mytobj.join();
    mytobj.detach();
    cout<<"I love china1"<<endl;
    cout<<"I love china2"<<endl;
    cout<<"I love china3"<<endl;
    cout<<"I love china4"<<endl;
    return 0;
}

在这里插入图片描述
由于主线程先结束了,因此子线程无法继续在控制台上打印信息。

注:一旦调用了detach(),不能再用join().

1.4 joinable():判断是否可以成功使用join()或者detach()


int main(){
    //此时这个代码中有两个线程在同时在执行,同时执行两个任务
    thread mytobj(MyPrint);
    if(mytobj.joinable()){
        cout<<"it can join/detach"<<endl;
    }
    else
        cout<<"it can not join/detach"<<endl;
    //mytobj.join();
    mytobj.detach();
    if(mytobj.joinable()){
        cout<<"2:it can join/detach"<<endl;
    }
    else
        cout<<"2:it can not join/detach"<<endl;

    cout<<"I love china1"<<endl;
    cout<<"I love china2"<<endl;
    cout<<"I love china3"<<endl;
    cout<<"I love china4"<<endl;
    return 0;
}

二、其他创建线程的方法

thread mytobj(MyPrint);//使用一个函数作为参数,该参数称为可调用对象

可调用对象除了函数之外,还有其他的。

2.1 类对象作为可调用对象

#include<iostream>
#include<vector>
#include<thread>
using namespace std;

class TA
{
    public:
    //重载()
        void operator()(){//不能带参数
            cout<<"我的Class线程开始1"<<endl;
            cout<<"我的Class线程结束1"<<endl;
        }
};

int main(){
    TA ta;
    thread mytobj3(ta);
    mytobj3.join();
    //mytobj3.detach();

    cout<<"I love china1"<<endl;
    cout<<"I love china2"<<endl;
    cout<<"I love china3"<<endl;
    cout<<"I love china4"<<endl;
    return 0;
}

输出:

我的Class线程开始1
我的Class线程结束1
I love china1
I love china2
I love china3
I love china4

注意:使用类对象作为可调用对象时,使用detach()可能会出现的问题。

class TA
{
    public:
        int &m_i;//成员变量是个引用
        //构造函数的参数也是个引用
        TA(int &i):m_i(i){}
        //重载()
        void operator()(){//不能带参数
            cout<<"m_i 1的值为"<<m_i<<endl;
            cout<<"m_i 2的值为"<<m_i<<endl;
            cout<<"m_i 3的值为"<<m_i<<endl;
            cout<<"m_i 4的值为"<<m_i<<endl;
            cout<<"m_i 5的值为"<<m_i<<endl;
        }
};

int main(){
    int my_i=6;
    TA ta(my_i);
    thread mytobj3(ta);
    //mytobj3.join();
    mytobj3.detach();

    cout<<"I love china1"<<endl;
    cout<<"I love china2"<<endl;
    cout<<"I love china3"<<endl;
    cout<<"I love china4"<<endl;
    return 0;
}

如上面这段代码,使用detach之后,倘若主线程在子线程之前执行完毕,main函数中的局部变量my_i就会被系统回收内存,此时该内存内的值是不可预知的。但是子线程还没执行完毕,还在打印这一内存内的值,就会产生不可预料的后果。

m_i 1的值为I love china1
I love china2
I love china3
I love china4
6
m_i 2的值为0
m_i 3的值为0
m_i 4的值为0
m_i 5的值为0

问题:一旦调用detach()之后,主线程若先于子线程结束,main函数中的TA对象也是局部的,也会被系统回收内存,为什么不会出错呢?

答:这个对象实际上是被复制到线程中去的,所以执行完主线程之后,对象ta会被摧毁,但是所复制的TA对象依旧存在。因此,只要这个TA类对象中没有引用或指针,就不会产生问题。

2.2 使用lambda表达式作为可调用对象创建线程

int main(){
    //使用lambda表达式创建线程
    auto my_lambda_thread=[]{
        cout<<"我的lambda线程开始执行了"<<endl;
        cout<<"我的lambda线程结束执行了"<<endl;
    };
    thread mytobj4(my_lambda_thread);
    mytobj4.join();
    //mytobj4.detach();
    cout<<"I love china1"<<endl;
    return 0;
}

文章内容来源《C++并发与多线程视频课程》

《UNIX环境高级编程编译源代码报错apue.h没有那个文件或目录

下载APUE源代码

在异步社区官网中找到《UNIX环境高级编程》页面,在其下方的配套资源中即可下载源码。

写好代码后,使用书上的方法编译出现问题:

cc myls.c

myls.c:1:10: fatal error: apue.h: 没有那个文件或目录
    1 | #include "apue.h"
      |          ^~~~~~~~
compilation terminated.

apue.h是作者自己封装的头文件,包含了某些标准系统头文件,定义了许多常量及函数原型。使用之前需要对其进行编译,生成对应的静态链接库,才能使用。

编译

cd apue.3e

make

  • 错误一:

    首先遇到的是“没有权限”的问题,解决:sudo chmod 777 * -R ,将该目录及子目录的权限设置为最高;


  • 错误二:

    make[1]: Entering directory '/home/pi/Downloads/apue.3e/filedir'
    gcc -ansi -I../include -Wall -DLINUX -D_GNU_SOURCE  access.c -o access  -L../lib -lapue 
    gcc -ansi -I../include -Wall -DLINUX -D_GNU_SOURCE  cdpwd.c -o cdpwd  -L../lib -lapue 
    gcc -ansi -I../include -Wall -DLINUX -D_GNU_SOURCE  changemod.c -o changemod  -L../lib -lapue 
    gcc -ansi -I../include -Wall -DLINUX -D_GNU_SOURCE  devrdev.c -o devrdev  -L../lib -lapue 
    devrdev.c: In function ‘main’:
    devrdev.c:19:25: warning: implicit declaration of function ‘major’ [-Wimplicit-function-declaration]
       printf("dev = %d/%d", major(buf.st_dev),  minor(buf.st_dev));
                             ^~~~~
    devrdev.c:19:45: warning: implicit declaration of function ‘minor’; did you mean ‘mknod’? [-Wimplicit-function-declaration]
       printf("dev = %d/%d", major(buf.st_dev),  minor(buf.st_dev));
                                                 ^~~~~
                                                 mknod
    /usr/bin/ld: /tmp/ccUJTVJZ.o: in function `main':
    devrdev.c:(.text+0x88): undefined reference to `major'
    /usr/bin/ld: devrdev.c:(.text+0x9c): undefined reference to `minor'
    /usr/bin/ld: devrdev.c:(.text+0xfc): undefined reference to `major'
    /usr/bin/ld: devrdev.c:(.text+0x110): undefined reference to `minor'
    collect2: error: ld returned 1 exit status
    make[1]: *** [Makefile:18: devrdev] Error 1
    make[1]: Leaving directory '/home/pi/Downloads/apue.3e/filedir'
    make: *** [Makefile:6: all] Error 1
    

    解决:

    filedir/devrdec.c中,添加头文件:#include <sys/sysmacros.h>


  • 错误三

    gcc -ansi -I../include -Wall -DLINUX -D_GNU_SOURCE  options.c -o options  -L../lib -lapue 
    make[1]: Leaving directory '/home/pi/Downloads/apue.3e/standards'
    making stdio
    make[1]: Entering directory '/home/pi/Downloads/apue.3e/stdio'
    gcc -ansi -I../include -Wall -DLINUX -D_GNU_SOURCE  buf.c -o buf  -L../lib -lapue 
    buf.c: In function ‘is_unbuffered’:
    buf.c:99:13: error: ‘FILE’ {aka ‘struct _IO_FILE’} has no member named ‘_flag’; did you mean ‘_flags’?
      return(fp->_flag & _IONBF);
                 ^~~~~
                 _flags
    buf.c: In function ‘is_linebuffered’:
    buf.c:105:13: error: ‘FILE’ {aka ‘struct _IO_FILE’} has no member named ‘_flag’; did you mean ‘_flags’?
      return(fp->_flag & _IOLBF);
                 ^~~~~
                 _flags
    buf.c: In function ‘is_unbuffered’:
    buf.c:100:1: warning: control reaches end of non-void function [-Wreturn-type]
     }
     ^
    buf.c: In function ‘is_linebuffered’:
    buf.c:106:1: warning: control reaches end of non-void function [-Wreturn-type]
     }
     ^
    make[1]: *** [Makefile:16: buf] Error 1
    make[1]: Leaving directory '/home/pi/Downloads/apue.3e/stdio'
    make: *** [Makefile:6: all] Error 1
    

    解决:

    修改stdio/buf.c ,将fp->_flag改为fp->_flags

    fp->_basefp->_ptr分别改为fp->_IO_read_base fp->_IO_read_ptr ,这两个是我根据FILE结构体的定义猜的。

    image-20211125162329143

编译成功的基础上,我们进行安装apue.h文件及其对应的静态链接库libapue.a

sudo cp ./include/apue.h /usr/include/

sudo cp ./lib/libapue.a /usr/local/lib/

为什么要将libapue.a移到/usr/local/lib中呢?原因是libapue.a是apue.h头文件中包含的所有函数及宏定义的具体实现,是一个静态链接库。

查看ld.conf.d/libc.conf你会发现gcc在搜索链接库的时候默认会去搜索/usr/local/lib/中的文件,所以我们将其放在这里,一劳永逸。

运行

gcc myls.c -o myls -lapue (libapue.a在这里要写成apue

./myls /dev

  • Copyrights © 2022-2024 lk
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信