C++并发与多线程(一)

最近开始学习C++多线程内容,将笔记整理发布,可供参考和自己复习

开发环境

基于C++11标准,环境为vs2019

并发、进程、线程的基本概念和综述

并发

两个或者更多的任务(独立的活动)同时发生(进行):一个程序同时执行多个独立的任务;

以往计算机为单核cpu,进程切换是根据时间的上下文切换,产生并发的假象,这种上下文切换是由时间开销的。

硬件发展,出现多处理器多核计算机,能够真正并行实现多个任务(硬件并发)。

可执行程序

磁盘上的一个文件,例如:Windows下的一个扩展名为.exe的文件。Linux下的 ls, -la文件。

进程

已知可执行程序能够运行。Windows下双击一个可执行程序来运行或者Linux下 ./文件名之后,可执行从运行起来,就叫做创建了一个进程。(并不是很准确)

线程

每个进程都有一个主线程,并且这个主线程是唯一的,也就是一个进程中只能有一个主线程

当你执行可执行程序,产生了一个进程后,这个主线程就随着这个进程启动了。运行程序时实际上时进程的主线程来调用代码的。

线程就是执行代码的!主线程和进程是同时存在也是同时消失的!

除了主线程外,可以通过自己写代码来创建其他的线程,其他线程走的是别的道路,执行不同的代码流程。每创建一个新线程,就可以在同一时刻多做一个不同的事情(多走一条不同的代码路径)。

多线程(并发)不是越多越好,每个线程都需要一个独立的堆栈空间。线程之间的切换要保存很多中间状态,上下文切换很频繁,耗费本该属于程序运行的时间。

线程启动、结束、创建多线程法、join、detach

线程的开始和结束

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

主线程从main函数开始执行。自己的线程也需要从一个线程开始执行(初始函数),一旦这个函数运行完毕,代表线程运行结束。

整个进程是否执行完毕的标志是主线程是否执行完,如果主线程执行完毕了,就代表整个进程执行完毕了;此时,一般情况下,如果其它子线程还没有执行完毕,那么这些子线程也会被操作系统强行终止。

所以,一般情况下:想保持子线程的运行状态的话,那么就要让主线程一直保持运行,不要让主线程运行完毕。

#include<thread>
#include <iostream>
using namespace std;
//创建自己的函数
void myprint()
{
    cout << "子线程开始执行" << endl;
    //.........
    //........
    cout << "子线程执行完毕" << endl;
}

int main()
{
    //(a) 包含头文件thread
    //(b) 创建函数
    //(c) main中开始写代码
    thread mytobj(myprint);//myprint可调用对象
    mytobj.join();

    cout << "主线程执行完毕 " << endl;

    return 0;
}

运行结果:

子线程开始执行
子线程执行完毕
主线程执行完毕

此程序中,主线程和子线程在同时运行,无论那一条线被阻塞,另一条线都可以运行。

thread类

thread是标准库里的一个类。thread mytobj(myprint);创建了线程,线程执行起点myprint(); (2) myprint开始执行。

join()

join()阻塞主线程,让主线程等待子线程执行完毕,然后主线程和子线程汇合,然后主线程才往下运行。mytobj.join();主线程阻塞到等待myprint()执行完毕,主线程就继续往下执行。

如果主线程执行完毕,子线程没有执行完毕,程序是不稳定不合格的。一个书写良好的程序,应该是主线程等待子线程执行完毕后,自己才能退出。

detach()

一般情况下,主线程要等待子线程执行完毕,自己再最后退出。

detach()主线程不和子线程汇合,主线程不必等子线程执行结束,并不影响子线程的执行。

为什么引入detach(): 如果创建很多子线程,让主线程等待子线程结束,不是很好,所以引入detach()。

一旦detach之后,与这个主线程关联的thread对象就会失去与主线程的关联。此时这个子线程就会驻留在后台运行,这个子线程就相当于被C++运行时库接管,当这个子线程执行完毕后,由运行时库负责清理该线程的相关资源(守护线程)。

#include<thread>
#include <iostream>
using namespace std;
//创建自己的函数
void myprint()
{
    cout << "子线程开始执行" << endl;
    //.........
    //........
    cout << "子线程执行完毕1" << endl;
    cout << "子线程执行完毕2" << endl;
    cout << "子线程执行完毕3" << endl;
    cout << "子线程执行完毕4" << endl;
    cout << "子线程执行完毕5" << endl;
    cout << "子线程执行完毕6" << endl;
    cout << "子线程执行完毕7" << endl;
    cout << "子线程执行完毕8" << endl;
    cout << "子线程执行完毕9" << endl;
    cout << "子线程执行完毕10" << endl;
}

int main()
{
    //(a) 包含头文件thread
    //(b) 创建函数
    //(c) main中开始写代码
    thread mytobj(myprint);
    mytobj.detach();

    cout << "主线程执行完毕1 " << endl;
    cout << "主线程执行完毕2 " << endl;
    cout << "主线程执行完毕3 " << endl;
    cout << "主线程执行完毕4 " << endl;
    cout << "主线程执行完毕5 " << endl;
    cout << "主线程执行完毕6 " << endl;
    cout << "主线程执行完毕7 " << endl;
    cout << "主线程执行完毕8 " << endl;
    cout << "主线程执行完毕9 " << endl;
    cout << "主线程执行完毕10 " << endl;
    cout << "主线程执行完毕11 " << endl;
    cout << "主线程执行完毕12 " << endl;
    cout << "主线程执行完毕13" << endl;
    cout << "主线程执行完毕14 " << endl;
    cout << "主线程执行完毕15" << endl;
    cout << "主线程执行完毕16 " << endl;
    cout << "主线程执行完毕17 " << endl;
    cout << "主线程执行完毕18 " << endl;

    return 0;
}

执行结果:

主线程执行完毕1
主线程执行完毕2
主线程执行完毕3
主线程执行完毕4
主线程执行完毕5
主线程执行完毕6
主线程执行完毕7
主线程执行完毕8
主线程执行完毕9
主线程执行完毕10
主线程执行完毕11
主线程执行完毕12
主线程执行完毕13
主线程执行完毕14
主线程执行完毕15
主线程执行完毕16
主线程执行完毕17
主线程执行完毕18
子线程开始执行

vs2019似乎改为了主线程先执行完毕然后在运行时库中运行子线程,也有可能是cpu的问题?

**一旦调用了detach(),就不能再用join();**否则系统报告异常。

joinable()

判断能否成功调用 join() 或者 detach() 。返回 true 或者 false 。

int main()
{
    //(a) 包含头文件thread
    //(b) 创建函数
    //(c) main中开始写代码
    thread mytobj(myprint);
    if (mytobj.joinable())
    {
        cout << "1:joinable == ture" << endl;
    }
    else
    {
        cout << "1:joinable == false" << endl;
    }
    mytobj.detach();
    if (mytobj.joinable())
    {
        cout << "2:joinable == ture" << endl;
    }
    else
    {
        cout << "2:joinable == false" << endl;
    }

结果为:

1:joinable == ture
2:joinable == false

其他创建线程的手法

类的方法
#include<thread>
#include <iostream>
using namespace std;
class TA
{
public:
    void operator()()//不能带参数  重载()操作符使这个函数变为可调用对象
    {
        cout << "子线程opertator()开始执行" << endl;
        //。。。。
        cout << "子线程opertator()执行完毕" << endl;
    }
};

int main()
{
    TA ta;
    thread mytobj3(ta);//ta是个可调用对象.
    mytobj3.join();
    cout << "主线程执行完毕1 " << endl;
    return 0;
}

结果:

子线程operator()开始执行
子线程operator()执行完毕
主线程执行完毕1

容易出现的问题:

#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;
class TA
{
public:
    int& m_i;
    TA(int& i) :m_i(i){}
public:
    void operator()()//不能带参数  重载()操作符使这个函数变为可调用对象
    {
        cout << "m_i1的值为:" << m_i << endl;
        cout << "m_i2的值为:" << m_i << endl;
        cout << "m_i3的值为:" << m_i << endl;
        cout << "m_i4的值为:" << m_i << endl;
        cout << "m_i5的值为:" << m_i << endl;
        cout << "m_i6的值为:" << m_i << endl;
    }
};


int main()
{
    int myi = 6;
    TA ta(myi);
    thread mytobj3(ta);//ta是个可调用对象.
    mytobj3.detach();
    cout << "主线程执行完毕1 " << endl;
    Sleep(10000);
    return 0;
}

int& m_i;使用引用,引用为取地址符,此时主线程执行完成后,m_i被主线程释放,detach() 之后调用的m_i就会出错。所以最好不要用引用构建对象。

为何调用death() ,主线程执行结束后,对象 ta 还可以调用?

首先,对象 ta 已经被析构,但是对象 ta 实际上是被复制到线程中去了,所以执行完主线程后,ta 被销毁,但是复制的 ta 对象依旧存在,只要对象中没有引用,指针,就不会产生问题。

修改代码中类的程序来查看对象:

class TA
{
public:
    int& m_i;
    TA(int& i) :m_i(i)
    {
        cout << "TA构造函数执行" << endl;
    }
    TA(const TA& ta):m_i(ta.m_i)
    {
        cout << "TA的拷贝构造函数被执行" << endl;
    }
    ~TA()
    {
        cout << "TA的析构函数被执行" << endl;
    }
public:
    void operator()()//不能带参数  重载()操作符使这个函数变为可调用对象
    {
        //cout << "子线程opertator()开始执行" << endl;
        。。。。
        //cout << "子线程opertator()执行完毕" << endl;
        cout << "m_i1的值为:" << m_i << endl;
        cout << "m_i2的值为:" << m_i << endl;
        cout << "m_i3的值为:" << m_i << endl;
        cout << "m_i4的值为:" << m_i << endl;
        cout << "m_i5的值为:" << m_i << endl; 
        cout << "m_i6的值为:" << m_i << endl;
    }
};

使用join()运行结果:

TA构造函数执行
TA的拷贝构造函数被执行
TA的析构函数被执行
主线程执行完毕1
TA的析构函数被执行

可以看见,在执行子线程时,TA执行了一次拷贝构造函数,证实了上面的说法。

第一次析构是线程中复制的对象析构,第二次是主线程析构对象。

lambda表达式创建线程
#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;
int main()
{
    auto mylamthread = [] {
        cout << "lambda子线程开始执行了" << endl;
    };
    thread mytobj4(mylamthread);
    mytobj4.join();
    cout << "主线程执行完毕1 " << endl;
    return 0;
}

执行结果:

lambda子线程开始执行了
主线程执行完毕1

线程传参详解,detach()的注意事项,成员函数做线程函数

传递临时对象作为线程参数 (1)

#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;


void myprint(const int& i, char* pmybuf)
{
    cout << i << endl;
    cout << pmybuf << endl;
    return;
}
int main()
{
    int mvar = 1;
    int& mvary = mvar;
    char mybuf[] = "this is a test!";
    thread mytboj(myprint, mvar, mybuf);
    mytboj.join();
    cout << "主线程执行完毕 " << endl;


    return 0;
}

运行结果:

1
this is a test!
主线程执行完毕

要避免的陷阱

对于传入参数i

上述程序看似没有问题,若是将 mytboj.join();改为 mytboj.detach(); 由于传入参数是一个引用,所以需要debug看一下变量的地址,实际操作下来:mvar 的地址 = 0x00c8fe8c ; mvary地址 = 0x00c8fe8c ,地址相同。i的地址 = 0x00f31314 ;可以看到,i的地址和mvar、mvary的地址不同,虽然传入是一个引用,但是实际上是复制了值。不会出现主线程执行完毕后释放i子线程不能调用的问题。

对于传入参数mybuf:

根据上述操作,找到地址:mybuf = 0x003af768 ;pmybuf = 0x003af768 ;

说明pmybuf使用时可能已经被系统回收。所以,指针再detach时一定会出现问题。

所以字符串怎么能够传入线程中?

传参改为:void myprint(const int& i, const string &pmybuf),两个地址很明显会不同。但是程序依然存在问题。

上面代码只是将char隐式转换为string。问题是:mybuf是在什么时候被隐式转换为string的?

事实上,存在mybuf都被回收了(main函数执行完)才开始将mybuf转换为string。所以这个方式是不稳定的。

稳定的方式如下:

#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;


//void myprint(const int& i, char* pmybuf)
void myprint(const int& i, const string  &pmybuf)//这里不用引用会拷贝三次,造成内存浪费!
{
    cout << i << endl;
    cout << pmybuf.c_str() << endl;
    return;
}
int main()
{
    int mvar = 1;
    int& mvary = mvar;
    char mybuf[] = "this is a test!";
    thread mytboj(myprint, mvar, string(mybuf));//将mybuf直接转换为string,可以保证不出问题!
    //mytboj.join();
    mytboj.detach();
    cout << "主线程执行完毕 " << endl;
    Sleep(1000);

    return 0;

所以在创建线程的同时构建临时对象的方法是可行的。

说这么多,建议不适用detach(). = =!

传递临时对象作为线程参数 (2)

线程ID的概念

id是一个数字,不管是主线程还是子线程,实际上都对应着一个数字,而且每一个线程对应的线程id不同。

线程id可以用C++标准库里的函数来获取。 std::this_thread::get_id()来获取

#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;

class A
{
public:
    int m_i;
    A(int a) :m_i(a) { cout << "A::A(int a)的构造函数执行" << this << "threadId = " << std::this_thread::get_id()  << endl; }
    A(const A& a):m_i(a.m_i){ cout << "A::A(const A)的拷贝构造函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
    ~A(){ cout << "A::A()的析构函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
};

void myprint2(const A& pmybuf)
{
    cout << "子线程myprint2的参数地址是:" << &pmybuf <<"threadId = " << std::this_thread::get_id() << endl;
}

int main()
{
    cout << "主线程ID是:" << std::this_thread::get_id() << endl;
    int mvar = 1;
    thread mytobj(myprint2, mvar);
    mytobj.join();
    cout << "主线程执行完毕 " << endl;


    return 0;
}

运行结果:

主线程ID是:15716
A::A(int a)的构造函数执行00B3F94CthreadId = 3224
子线程myprint2的参数地址是:00B3F94CthreadId = 3224
A::A()的析构函数执行00B3F94CthreadId = 3224
主线程执行完毕

可以看见,构造函数是在子线程3224中构造的,也是在子线程中析构的。如果不是join而是detach的话,变量已经在主线程中释放,子线程再构造时会出现不可预料的问题。

采用之前所说的直接转换对象,可以解决此问题,将上面 thread mytobj(myprint2, mvar);改为 thread mytobj(myprint2, A(mvar));之后,运行结果:

主线程ID是:20148
A::A(int a)的构造函数执行00EFFBD0threadId = 20148
A::A(const A)的拷贝构造函数执行01041370threadId = 20148
A::A()的析构函数执行00EFFBD0threadId = 20148
子线程myprint2的参数地址是:01041370threadId = 32688
A::A()的析构函数执行01041370threadId = 32688
主线程执行完毕

很明显可以看出传递对象是在主线程执行之前在主线程中构造20148出来。

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

传递类对象作为线程参数
#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;

class A
{
public:
    mutable int m_i; //mutable 不管在什么情况下,m_i都可以修改
    A(int a) :m_i(a) { cout << "A::A(int a)的构造函数执行" << this << "threadId = " << std::this_thread::get_id()  << endl; }
    A(const A& a):m_i(a.m_i){ cout << "A::A(const A)的拷贝构造函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
    ~A(){ cout << "A::A()的析构函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
};

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

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

在这个程序中,修改了成员变量m_i的值,但是因为之前的特性,修改的只是线程中的拷贝构造函数所构造的对象成员,main函数中的成员并没有被修改。所以main函数中的m_i依然是10;

std::ref函数可以把引用的参数传递到线程中去。thread mytobj(myprint2,myobj);改为 thread mytobj(myprint2,std::ref(myobj));,并且此时可以去掉 void myprint2(const A& pmybuf)中的const。就是说传递的是真的引用,而不是拷贝的对象。

传递智能指针作为线程参数
#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;

class A
{
public:
    mutable int m_i; //mutable 不管在什么情况下,m_i都可以修改
    A(int a) :m_i(a) { cout << "A::A(int a)的构造函数执行" << this << "threadId = " << std::this_thread::get_id()  << endl; }
    A(const A& a):m_i(a.m_i){ cout << "A::A(const A)的拷贝构造函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
    ~A(){ cout << "A::A()的析构函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
};

void myprint2(unique_ptr<int> pzn)//智能指针
{
    //pmybuf.m_i = 199;//修改该值不会影响到main函数
    cout <<"threadId = " << std::this_thread::get_id() << endl;
}

int main()
{
    unique_ptr<int> myp(new int(100));//独占智能指针
    thread mytobj(myprint2,std::move(myp));//std::move()转移独占智能指针
    mytobj.join();
    return 0;
}

子线程中的pzn和主线程地址一样,所以用detach()会出现错误。(和前面原理一样)

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

#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;

class A
{
public:
    mutable int m_i; //mutable 不管在什么情况下,m_i都可以修改
    A(int a) :m_i(a) { cout << "A::A(int a)的构造函数执行" << this << "threadId = " << std::this_thread::get_id()  << endl; }
    A(const A& a):m_i(a.m_i){ cout << "A::A(const A)的拷贝构造函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
    ~A(){ cout << "A::A()的析构函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }

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

int main()
{
    A myobj(10);
    thread mytobj(&A::thread_work,myobj,15 );
    mytobj.join();
    return 0;
}

执行结果:

A::A(int a)的构造函数执行010FF9B4threadId = 12416
A::A(const A)的拷贝构造函数执行013915B4threadId = 12416
子线程执行013915B4threadId = 4888
A::A()的析构函数执行013915B4threadId = 4888
A::A()的析构函数执行010FF9B4threadId = 12416

拷贝构造函数执行,可以用detach(),节约空间使用std::ref(),或者&myobj的时候就可以真正的传递引用,不能用detach().

重载()的情况:

#include<thread>
#include <iostream>
#include <windows.h>
using namespace std;

class A
{
public:
    mutable int m_i; //mutable 不管在什么情况下,m_i都可以修改
    A(int a) :m_i(a) { cout << "A::A(int a)的构造函数执行" << this << "threadId = " << std::this_thread::get_id()  << endl; }
    A(const A& a):m_i(a.m_i){ cout << "A::A(const A)的拷贝构造函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }
    ~A(){ cout << "A::A()的析构函数执行" << this << "threadId = " << std::this_thread::get_id() << endl; }

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

void myprint2(unique_ptr<int> pzn)//智能指针
{
    //pmybuf.m_i = 199;//修改该值不会影响到main函数
    cout <<"threadId = " << std::this_thread::get_id() << endl;
}

int main()
{
    //unique_ptr<int> myp(new int(100));//独占智能指针
    //thread mytobj(myprint2,std::move(myp));//std::move()转移独占智能指针
    A myobj(10);
    thread mytobj(myobj,15 );
    mytobj.join();
    return 0;
}

运行结果:

A::A(int a)的构造函数执行012FFB64threadId = 33916
A::A(const A)的拷贝构造函数执行0154148CthreadId = 33916
子线程执行0154148CthreadId = 11580
A::A()的析构函数执行0154148CthreadId = 11580
A::A()的析构函数执行012FFB64threadId = 33916

依然要拷贝构造函数,见上一个知识点。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

word_no_bug

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值