39、C++11多线程及其学习笔记

>博客是一个拾忆的过程,无形中节约了你的生命<

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

一、线程的创建

1.1、使用类的成员函数作为线程入口

#include 
#include 
#include 
class MyThread {
public:
    MyThread() {};
    ~MyThread() {};
    //线程相关
    void ThreadFunction() {//线程入口函数
        int i = 0;
        while (true) {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            std::cout << "th_prt i = " << ++i<<",th_id = " << std::this_thread::get_id() << "\r\n";
            //退出循环判断
            if (mtx.try_lock()) {
                if (!th_status) {
                    mtx.unlock();
                    break;
                }
                mtx.unlock();
            }
        }
        std::cout << "thread exit" << "\r\n";
    }
    std::mutex mtx; //定义互斥量
    bool th_status = true; //线程状态
private:
};
int main()
{
    MyThread th;
    std::thread mytobjob(&MyThread::ThreadFunction, &th);//创建线程并运行
    std::this_thread::sleep_for(std::chrono::milliseconds(500));//延时0.5s
    //一、让线程退出循环
    th.mtx.lock();
    th.th_status = false;
    th.mtx.unlock();
    //二、阻塞主线程并等待子线程退出<因为子线程循环已结束,这里的阻塞时间几乎为零>
    mytobjob.join();
    std::cout << "END\r\n";
}
/***************************************************************************************/
说明:
01、thread ->是标准库里面的类。
02、std::thread mytobjob(func); ->创建线程mytobjob并执行,传入函数名作为线程开始入口。
03、mytobjob.join(); ->阻塞主线程并等待子线程结束(注意它是个结束线程的函数)。
04、线程的函数执行完毕,线程并没有完全退出,只是停止了而已。
05、子线程安全退出步骤总结:
    设置标志位让子线程函数退出循环->调用join()函数结束子线程。
06、必须先让子线程退出循环再调用join()函数,因为join()函数会阻塞主线程。
07、主线程默认使用main()作为入口函数。
08、mytobjob.detach(); ->子线程与主线程分离,两者不在关联,主线程退出后,由后台接管。(注意这句        
    话,可能会让人误解,主线程结束时会退出进程,detach()的线程也会被系统回收,除非你阻止主线程 
    退出进程。总之进程一旦结束,其下的线程都会被回收)
09、mytobjob.joinable(); ->返回true和false,判断是否可以.join(),比如线程已经退出,就会返回false。
/***************************************************************************************/

1.2、使用类的重载运算函数作为函数入口点

<---------------------------------------------------------------------------->
1、mythread.h
#ifndef _MYTHREAD_H
#define _MYTHREAD_H
#include 
#include 
class MyThread {
private:

public:
    MyThread(); 
    ~MyThread();
    //线程相关
    std::mutex mtx;
    bool thread_flag = true; //线程跳出循环
    //重载运算符作为线程入口
    void operator()()//注意此函数执行时会拷贝一份类对象,所以并不与类公用成员变量
    {                //而是复制出来的,一般不用这种方式
        int i = 0;
        while (true){
            std::this_thread::sleep_for(std::chrono::milliseconds(500));
            std::cout << "thraad prt i =" << i << "\r\n";
            i++;
            //线程退出循环判断
            this->mtx.lock();
            if (this->thread_flag == false){
            this->mtx.unlock();
            break;
        } 
        this->mtx.unlock();
        }
        std::cout << "thraad exit"<< "\r\n";
    }
};
#endif
<---------------------------------------------------------------------------->
2、mythread.cpp
#include "mythread.h"
//构造函数
MyThread::MyThread(){
    std::cout<<"constructor prt:"<<"pcls = "<
3、main.cpp
#include 
#include 
#include "mythread.h"
int main()
{
    MyThread th;
    std::thread mytobjob(th);
    //一、让线程退出循环
    th.mtx.lock();
        th.thread_flag = false; //注意此句并不能让线程退出循环
                                //因这个是对象的成员,线程函数使用的是它自己复制出来的
                                //所以程序的运行现象会一直阻塞
    th.mtx.unlock();
    //二、阻塞主线程并退出
    mytobjob.join();
    std::cout << "END\r\n";
}
<---------------------------------------------------------------------------->
01、要想使线程操做原对象的成员变量,可以用引用或指针,让引用在构造函数时绑定成员变量,因为引用复制后它绑定的还是原来的那个变量。(指针复制一份,它指向的内存不变)
02、注意,用指针和引用在修改原对象的成员变量时,必须保证原对象没有被析构,否则会产生严重后果。
03、可以自己定义拷贝构造函数来确定怎么拷贝。
04、重点:std::thread mytobjob(std::ref(th));//以引用的形式作为形参,可以解决上述问题
    这种写法只能用mytobjob.join();退出,因为子线程中使用的是主线程中的对象
05、引用和std::ref(th)在这里起到的效果不一样
    引用:先复制一份原对象,引用绑定到赋值出来的对象身上    
    std::ref(th):返回reference_wrapper类型,注意这个和引用&是有区别的,其可以阻止中间变量的    
                  产,使用方法和引用一样,也可以赋值给引用类型。    
                  生,感兴趣的可以查查相关资料。

1.3、用lambda表达式作为线程入口

#include 
#include 
#include "mythread.h"
int main()
{
    auto mylamthread = []{
        std::cout << "我的lam线程开始执行\r\n";
        //.......
        std::cout << "我的lam线程执行结束\r\n";
    };
    std::thread mytobjoblam(mylamthread);
    mytobjoblam.join();
    return 0;
}
//单独一个函数作为线程入口的方法与此相同

二、线程的传参

2.1、主线程隐式创建临时对象的问题

#include 
#include 
#include "mythread.h"
int main()
{
    auto mylamthread = [](const int &i,char *pmybuf){
        std::cout <<"形参 i = "<

39、C++11多线程及其学习笔记_第1张图片

 A:看似没有问题的传递,但暗藏玄机,其实线程函数i绑定的并不是mvar,而是先拷贝一份mvar,而i绑定的拷贝的那个变量。(线程函数一般会搞复制这一套,其实是为了避免主线程退出之后,释放了变量,而访问了不可知的变量=>引用不是真引用、但保证了线程的安全)

B:需要注意的是第二个指针类型形参pmybuf和传入却是一个地址,因为无论指针怎么拷贝,指针指向的地址是不变的,但同时这种操作很危险,一定要要确保主线程结束之后,再释放指针指向的变量,注意指针就是一个变量而已。(=>指针是真指针,但不能保证线程的安全)

C:变量的传递就不用说了,因为我们都知道,变量的传参就是拷贝而已,而不是传入本身。

C:断点调试使用说明:shift+F9

39、C++11多线程及其学习笔记_第2张图片

2.2、传参时发生的隐式类型转换问题

#include 
#include 
#include 
#include "mythread.h"
int main()
{
    auto lamfunc = [](const int& i,const std::string &strx) {
        std::cout << "形参 i = " << i << "\r\n";
        std::cout << "形参 pmybuf = " << strx.c_str() << "\r\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    };

    int mvar = 1;
    int& mvary = mvar;
    char str[] ="this is a test";
    std::thread mythread(lamfunc, mvar, str);
    mythread.join();
    std::cout << "The main thread end!\r\n";
}
//1、str的数组传入,类型不匹配就会被隐式转换产生中间变量,所以str和strx的地址不一样,线程是安全的。
//2、这个程序看似没问题,但是还是存在一个隐藏很深的bug........
     隐式转换是需要时间的,如果转换还没完成,str就被回收了,这个时候就会出现不可预料的结果。

陷阱二的解决办法

#include 
#include 
#include 
#include "mythread.h"
int main()
{
    auto lamfunc = [](const int& i,const std::string &strx) {
        std::cout << "形参 i = " << i << "\r\n";
        std::cout << "形参 pmybuf = " << strx.c_str() << "\r\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    };

    int mvar = 1;
    int& mvary = mvar;
    char str[] ="this is a test";
    std::thread mythread(lamfunc, mvar, std::string(str)); //创建的时候就转换
    mythread.join();
    std::cout << "The main thread end!\r\n";
}
/*
 *创建的时候就转换,让类型转换在主线程中完成。
 *陷阱二产生的根本原因,是转换在子线程中完成,而转换的条件是被转换的对象是存在的。
 *如果先转换,后执行线程,就不会出现此类问题。
 *总结1:线程的传的参数要在线程执行前准备完成,因为创建线程到线程成功运行中间应该是
        一个很短的时间,所以在主线程和从线程没共享资源的条件下,应尽快让两者脱离关系,不要
        在创建的时候拖泥带水,搞一堆事。(隐式转换就属于拖泥带水,非要在哪个间隙处理问题,资
        源提前准备好不可以吗?)
 *总结2:存在类型转换时,在创建线程时构造临时对象,形参使用引用类型。(重点、重点、重点)
 */

2.3、使用线程ID查看隐式类型转换在哪个线程中执行

C++标准库函数提供了线程ID的获取方法:std::this_thread::get_id()

示例代码一:
#include 
#include 
#include 
#include "mythread.h"
class A {
public:
    int m_i;
    A(int a) :m_i(a) {
        std::cout << "[A::A(int a)构造执行]," << this << ",thread_id=" << std::this_thread::get_id() << "\r\n";
    }
    A(const A& a) :m_i(a.m_i) {
        std::cout << "[A::A(const A)拷贝构造执行]," << this << ",thread_id =" << std::this_thread::get_id() << "\r\n";
    }
    ~A() {
        std::cout << "[A::~A()析构执行]," << this << ",thread_id =" << std::this_thread::get_id() << "\r\n";
    }
};
int main()
{
    std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
    auto lamfunc = [](const A &clsx) {
        std::cout << "子线程的参数clsx的地址 = "<<&clsx << ",thread_id =" << std::this_thread::get_id() << "\r\n";
    };
    int mvar = 1;
    std::thread mythread(lamfunc,mvar); //mvar隐式类型转换成A类型,会调用构造函数进行类型转换
    mythread.join();

    std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
    return 0;
}

39、C++11多线程及其学习笔记_第3张图片

示例代码二
#include 
#include 
#include 
#include "mythread.h"
class A {
public:
    int m_i;
    A(int a) :m_i(a) {
        std::cout << "[A::A(int a)构造执行]," << this << ",thread_id=" << std::this_thread::get_id() << "\r\n";
    }
    A(const A& a) :m_i(a.m_i) {
        std::cout << "[A::A(const A)拷贝构造执行]," << this << ",thread_id =" << std::this_thread::get_id() << "\r\n";
    }
    ~A() {
        std::cout << "[A::~A()析构执行]," << this << ",thread_id =" << std::this_thread::get_id() << "\r\n";
    }
};
int main()
{
    std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
    auto lamfunc = [](const A &clsx) {
        std::cout << "子线程的参数clsx的地址 = "<<&clsx << ",thread_id =" << std::this_thread::get_id() << "\r\n";
    };
    int mvar = 1;
    std::thread mythread(lamfunc,A(mvar)); //mvar显示类型转换成A类型,创建临时对象,临时对象被拷贝一份给引用绑定,然后临时对象析构释放,
    mythread.join();

    std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
    return 0;
}
/*

39、C++11多线程及其学习笔记_第4张图片

注意1:示例中可以得出结论,拷贝是安全的(C语言形参传递也是拷贝),类型转换不安全的。

所以注意尽量避免在子线程中进行类型转换。(重点、重点、重点)

注意2:关于我对线程传参的理解,如果你觉得认可就认可,不认可以去翻看linux操作系统源码,主线程不会把自己的变量直接进行传递,主线程会复制一份变量,把复制的这份变量交给子线程,子线程在执行的时候用这份复制的变量进行参数传递。 

1、主线程变量a,拷贝一份 a_x                     :此时a与a_x属于主线程的作用域。
2、创建线程时,让a_x的作用域主线程分离  :此时a_x属于子线程作用域。
3、子线程执行线程函数,首先要把a_x传入,(注意变量传入会再次拷贝)。
4、所以形参一般设置成引用,这样可以避免再次拷贝一次。
5、a_x的生命周期是其在线程函数执行结束才会被析构掉。
以上的作用域只是本人便于理解乱说的,但这样更容易理解。

2.4、类作为形参传递,引用类型的形参必须加const的问题

39、C++11多线程及其学习笔记_第5张图片

 1、为了线程安全,系统默认不允许修改拷贝的那份变量,所以引用类型不加const程序会报错。

2、临时对象不可修改,这是一个很强硬的原则问题。(修改也没啥意义,它都脱离主线程控制了)

3、临时对象虽然不能修改,但可以在传参时禁止主线程产生临时对象,方法如下:

#include 
#include 
#include 
#include "mythread.h"
class A {
public:
    int m_i;
    A(int a) :m_i(a) {
        std::cout << "[A::A(int a)构造执行]," << this << ",thread_id=" << std::this_thread::get_id() << "\r\n";
    }
    A(const A& a) :m_i(a.m_i) {
        std::cout << "[A::A(const A)拷贝构造执行]," << this << ",thread_id =" << std::this_thread::get_id() << "\r\n";
    }
    ~A() {
        std::cout << "[A::~A()析构执行]," << this << ",thread_id =" << std::this_thread::get_id() << "\r\n";
    }
};
int main()
{
    std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
    auto lamfunc = [](A &clsx) {
        std::cout << "子线程的参数clsx的地址 = " << &clsx << ",thread_id =" << std::this_thread::get_id() << "\r\n";
    };
    int mvar = 1;
    A v(mvar);
    std::thread mythread(lamfunc,std::ref(v));//方法std::ref()可避免向子线程传递变量时产生临时对象
    mythread.join();
    while (1);

    std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
    return 0;
}
//注意:如果禁止产生临时对象,引用形参操作的就是主线的作用域的变量,几乎等于操作全局变量。
//注意资源共享问题,这可是一个真引用。
//std::ref(v):此方法可阻止线程传参时产生临时对象。

2.5、智能指针作为线程函数的形参

std::unique_ptr myp(new int(100)); //这是C++基础,注意智能指针就是个模板类。
智能指针指向的是一块内存,如果让两个智能指针直接相等是语法上不允许的,因为一块内存不能被释放两次,需要先将指针转换为右值,才能使用,例如以下写法是错误的(编译器报错):

int main()
{
    std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";


    auto lamfunc = [](std::unique_ptr pzn) {
        std::cout << "子线程的参数clsx的地址 = " << *pzn << ",thread_id =" << std::this_thread::get_id() << "\r\n";
    };

    std::unique_ptr myp(new int(100)); //创建一个独占式智能指针myp指向一个new出来的int空间。
    std::thread mythread(lamfunc,myp);
    mythread.join();
    
    std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
    return 0;
}

为说明右值问题,如下先进行一次C++11右值基础的复习,加深印象。

左值:(lvalue) => 存放在内存空间,有确切的地址。
右值:(rvalue) => 不存放在内存空间,没有确切的地址。
std::move(变量);//返回传入参数的右值引用。对与左值和右值,请参考C++基础。
                //注意std会对变量造成破坏,故使用std::move后,变量尽量不要使用了。
复习零、类初始化和赋值是完全不同的两种概念
    class A{}      //定义一个类
    A cls1();      //初始化操作(调用的是构造函数)
    A cls2 = cls1; //初始化操作(调用的是构造函数)
    cls2 = cls1;   //赋值操作(调用的是赋值运算符)
    对于引用,初始化(也叫绑定)和赋值也是不同的概念,而且引用必须初始化。初始化之后的其他操作都是对其被绑定对象的操作
    以上我感觉对于学C++的人来说,意识到这点很重要。
复习一、三五法则
    对于C++11之前的版本,适用三法则,C++11,及其之后的版本适用五法则。(法则请百度)
    C++11的五法则做如下说明:
    默认赋值运算符函数是浅拷贝。移动构造函数一旦创建,默认赋值运算符函数将不复存在。
    移动构造函数要和赋值运算符构造函数一起创建(不使用赋值操作,可以忽略其函数不写)
    一般拷贝构造函数写深拷贝代码,移动构造函数写浅拷贝代码(但无论怎样,他们只能参与初始化操作)
    大白话:浅拷贝就是把对象的资源掏空,被掏空的对象就不要在使用了。
复习二、左值和右值
    int a = 1;     //a是一个左值,1是一个右值
    int b = 2;     //b是一个左值,2是一个右值
    int c = a + b; //+需要右值,所以a和b都转换成右值,并且返回一个右值
    函数和匿名对象都属于右值,右值也叫将亡值。
    左值能转换为右值,右值却不能转换为左值
    int arr[] = {1, 2};
    int* p = &arr[0];
    *(p + 1) = 10; //p+1是一个右值,但是*(p+1)是一个左值
    p+1 = 10;      //错误
复习三、左值引用和右值引用
    const int    与  int const   没有区别
    const int &  与  int const & 没有区别
    交换次序只有在指针时才有区别=>(int const * 和const int *有区别)
    int & 只能绑定左值
    int && 只能绑定右值,(注意右值也叫即将销毁的对象)
    const int & 左值和右值都可以绑定(const int &会将右值的生命周期延长到和自己的一样)
    右值引用是为了移动构造函数产生的,其实就是为了浅拷贝一个右值的资源,右值作为将亡值,毕竟属于即将销毁的值,但有时候我们不希望销毁,比如函数返回值,所以只能将其拷贝到一个变量中,然后其被析构,这中间的拷贝其实是一种浪费,移动构造函数可以避免拷贝带来的浪费。但移动构造也能拷贝左值,注意左值不是将亡值,如果对其进行移动构造,之后将以不要在使用这个左值。
复习五、匿名对象被扶正的问题
    匿名对象,使用匿名对象做初始化时,不执行拷贝,而是被扶正(匿名对象和函数都属于右值)
    例如:std::string str = std::string("haut school"); //右边的匿名对象会被扶正
复习六、移动构构造函数
    C++11新特性,右值引用类型 int &&,右值引用顾名思义是可以绑定右值的引用。
    旧版本的某些情况下,对象在拷贝之后直接销毁了,出现了很多的不必须要的拷贝等问题。
    所以,C++11引入了移动操作,右值引用就是为了支持对象移动而产生的。
    重点:右值引用其实就是为了偷一个对象的值,而非复制它。
          移动构造函数对传入的对象会造成一定程度的破坏。
复习七、指针
    定义指针的时候一定养成初始化的习惯,例如:char*p = nullptr;
    删除指针内存够一定养成初始默认的习惯,例如:if(p!=nullptr){ delete p;p=nullptr; }
代码示例:
#include 
class Buffer { 
public:
    int i = 99;
    char* buf = nullptr;   
    Buffer() {//构造1、无参构造函数
        buf = new char[10];
        buf[0] = 0; buf[1] = 1; buf[2] = 2;
        std::cout << "Buffer() this = " << this << "\r\n";
    }
    Buffer(const Buffer& cls_in):i(cls_in.i) {//构造2、拷贝构造(深拷贝)
        this->buf = new char[strlen(cls_in.buf)+1]; 
        strcpy_s(this->buf, strlen(cls_in.buf) + 1,cls_in.buf);
        std::cout << "Buffer(Buffer& param)" << this << "\r\n";
    }
    Buffer(Buffer&& cls_in) noexcept :i(cls_in.i) {//构造3、移动构造(移动拷贝)(注意此构造会删除默认赋值运算符函数)
        this->buf = cls_in.buf;
        cls_in.buf = nullptr;
        std::cout << "Buffer(Buffer&& param)" << this << "\r\n";
    }
    Buffer& operator=(const Buffer& cls_r) {//拷贝赋值运算符函数
        if (this == &cls_r) return *this; 
        this->i = cls_r.i;
        this->buf = new char[strlen(cls_r.buf) + 1];
        strcpy_s(this->buf, strlen(cls_r.buf) + 1, cls_r.buf);
        std::cout << "Buffer& operator=(const Buffer& cls_r)" << "\r\n";
        return *this;
    } 
    Buffer& operator=(Buffer&& cls_r) noexcept {
        if (this == &cls_r) return *this;
        this->i = cls_r.i;
        delete[] this->buf; //斩断自己
        this->buf = cls_r.buf; //指向别人
        cls_r.buf = nullptr;//斩断别人
        return *this;
    }
    ~Buffer() {//析构函数
        std::cout << "~Buffer() = " << this << "\r\n";
        if (buf != nullptr){
            delete[] buf;
            buf = nullptr;
        }
    }
};
int main()
{
    Buffer cls; cls.i = 9;
    Buffer cls1;
    cls1 = std::move(cls);
    std::cout << cls1.i << "\r\n";
    return 0;
}

经过上面的复习,大致可以得出解决办法,那就是使用std::move将原指针掏空就可以了。

int main()
{
    std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";


    auto lamfunc = [](std::unique_ptr pzn) {
        std::cout << "子线程的参数clsx的地址 = " << *pzn << ",thread_id =" << std::this_thread::get_id() << "\r\n";
    };

    std::unique_ptr myp(new int(100)); //创建一个独占式智能指针myp指向一个new出来的int空间。
    std::thread mythread(lamfunc,std::move(myp));
    mythread.join();
    
    std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
    return 0;
}

注意,new出来的变量依然是在主线程中new的,属于主线程,子线程中的智能指针依然指向的是属于主线程的
内存,所以这里依然要保持子线程先退出,主线程才能退出。因为主线程退出会释放属于自己的变量。故不能使用mythread.detach();

三、并发与多线程

前四章是单个线程的创建方法和传参时的一些注意点,接下来在以上的基础上进行多个线程创建。

3.1、创建多个线程

创建十个线程,并放入到容器中

#include 
#include 
#include 
void myprint(int inum) {
    std::cout << "Run:线程编号=" << inum <<",线程ID="<< std::this_thread::get_id() << std::endl;
    
    std::cout << "Stop:线程编号=" << inum << ",线程ID=" << std::this_thread::get_id() << std::endl;
}
int main()
{
    std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
    //--------------------------------------------------------------------------------------------//
    std::vector mythreads;//容器
    //创建10个线程,入口函数都使用void myprint(int inum) 
    for (int i = 0; i < 10; i++) {
        mythreads.push_back(std::thread(myprint,i));
    }
    for (auto iter = mythreads.begin(); iter!= mythreads.end(); ++iter) {
        iter->join();
    }
    //--------------------------------------------------------------------------------------------//
    std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
    return 0;
}

3.2、通用型互斥量

//所谓通用型互斥量,就是经常使用的独占式互斥量
#include 
#include 
#include 
#include 
#include 
//共享数据的保护案例<生产-消费模式>
//网络游戏服务器:
//     两个自己创建的线程,一个线程收集玩家发来的命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列中。
//     另一个线程从队列中去除玩家发来的命令,解析,然后执行相应的动作 。
//数据结构:vector,list。
//list和vector区别:<百度一下>             
class A {
public:
    //线程1:接受到玩家命令,插入到一个队列中(写)
    void inMsgRecvQueue() {
        for (int i = 0; i < 10000; ++i) {
            std::cout << "inMsgRecvQueue()执行,插入一个元素" << i << std::endl;
            mtx.lock();
                msgRecvQueue.push_back(i); //i就是收到的命令
            mtx.unlock();
        }
    }
    bool outMsgLULProc(int& command) {
        //std::lock_guard sbguard(mtx);//模板类,在析构的时候会调用mtx.unlock()
        //可以智能释放锁,但是没有mtx.lock();和mtx.unlock();灵活(具体用法可以百度)
        mtx.lock();
        if (!msgRecvQueue.empty()) {
            command = msgRecvQueue.front();

            msgRecvQueue.pop_front();
            mtx.unlock();
            return true;
        }
        mtx.unlock();
        return false;
    }
    //线程2:把数据从消息列表中取出的线程(读,删)
    void outMsgRecvQueue() {
        int command = 0;
        for (int i = 0; i < 10000; ++i) {
            bool ret = outMsgLULProc(command);
            if (ret == true) {//链表非空->取元素成功
                std::cout << "outMsgRecvQueue()执行,取出一个元素" << command << std::endl;
            }
            else {//链表空->取元素失败
                std::cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << std::endl;
            }
        }
    }
private:
    std::mutex mtx;
    std::list msgRecvQueue;//链表(消息队列),专门用于代表玩家发来的命令

};
int main()
{
    std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
    //--------------------------------//
    A myobj;
    std::thread myOutnMsgObj(&A::outMsgRecvQueue,&myobj);
    std::thread myInMsgObj(&A::inMsgRecvQueue, &myobj);
    myOutnMsgObj.join();
    myInMsgObj.join();
    //--------------------------------//
    std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
    return 0;
}

3.3、递归式互斥量

//同一个线程的同一个通用型互斥变量不能锁两次,但现实中有需要锁两次的场景,示例代码如下:
class A {
public:
    void testfun1() {
        std::lock_guard sbguard(mtx);
        //访问共享资源 
    }
    void testfun2() {
        std::lock_guard sbguard(mtx);
        //访问共享资源
        testfun1(); //此处会引发多次上锁异常
    }
private:
    std::mutex mtx;
};
//单独调用testfun1()和testfun2()时没有问题,但当两个函数互调时就会出现多次上锁的情况,此时程序
//会出现异常,为了解决此场景的问题,引入了递归式互斥量,示例代码如下:
class A {
public:
    void testfun1() {
        std::lock_guard sbguard(mtx);
        //访问共享资源
    }
    void testfun2() {
        std::lock_guard sbguard(mtx);
        testfun1(); //此处不会触发异常
    }
private:
    std::recursive_mutex mtx;
};
//说明1:std::recursive_mutex虽然可以多次锁定,但其执行效率没有std::mutex高。
//说明2:std::recursive_mutex的递归上锁次数有限制,超过限制时会触发异常。
//说明3:std::lock_guard<>是个类模板,用于构建区域锁,即对象创建构造时对传入的互斥量上锁,对象析构时对传入的互斥量解锁,用于实现自动释放锁的逻辑。

3.4、时间式互斥量

//所谓时间式互斥量就是带有超时功能的互斥量
//其有两个重要的方法:
//1、try_lock_for(); //参数是一个超时时间
//2、try_lock_unti(); //参数是未来的一个时间点
/***************************************************************************************/
//示例1:try_lock_for()的用法
#include 
#include 
#include 
#include 
#include 
class A {
public:
    //线程1:接受到玩家命令,插入到一个队列中(写)
    void inMsgRecvQueue() {
        for (int i = 0; i < 10000; ++i) {
            std::chrono::milliseconds timeout(100);
            if (tim_mtx.try_lock_for(timeout)) {//100ms内上锁成功
                std::cout << "in...()拿锁成功,插入元素" << i <<"成功"<< "\r\n";
                msgRecvQueue.push_back(i);
                tim_mtx.unlock();
            }
            else {//100ms内上锁未成功,超时了
                std::cout << "in...()拿锁失败,未插元素" << i << "失败" << "\r\n";
                std::chrono::milliseconds dura(100);
                std::this_thread::sleep_for(dura);
            }
        }
    }
    bool outMsgLULProc(int& command) {
            tim_mtx.lock();
            std::this_thread::sleep_for(std::chrono::milliseconds(200));
            if (!msgRecvQueue.empty()) {
                command = msgRecvQueue.front();
                //执行相应操作....
                msgRecvQueue.pop_front();
                tim_mtx.unlock();
                return true;
            }
            tim_mtx.unlock();
        return false;
    }
    //线程2:把数据从消息列表中取出的线程(读,删)
    void outMsgRecvQueue() {
        int command = 0;
        for (int i = 0; i < 10000; ++i) {
            bool ret = outMsgLULProc(command);
            if (ret == true) {//链表非空->取元素成功
                std::cout << "out...()执行,取出元素" << command << std::endl;
            }
            else {//链表空->取元素失败
                std::cout << "out...()执行,但目前消息队列为空"<< std::endl;
            }
        }
    }
private:
    std::timed_mutex tim_mtx;//时间式互斥量
    std::list msgRecvQueue;//链表(消息队列),专门用于代表玩家发来的命令
};
int main()
{
    std::cout << "The main start,thread_id = " << std::this_thread::get_id() << "\r\n";
    //--------------------------------//
    A myobj;
    std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobj);
    std::thread myInMsgObj(&A::inMsgRecvQueue, &myobj);
    myOutnMsgObj.join();
    myInMsgObj.join();
    //--------------------------------//
    std::cout << "The main end,thread_id = " << std::this_thread::get_id() << "\r\n";
    return 0;
}
/***************************************************************************************/
//示例2:try_lock_unti()的用法
#include 
#include 
#include 
#include 
#include 
class A {
public:
    //线程1:接受到玩家命令,插入到一个队列中(写)
    void inMsgRecvQueue() {
        for (int i = 0; i < 10000; ++i) {
            std::chrono::milliseconds timeout(100);
            if (tim_mtx.try_lock_until(std::chrono::steady_clock::now()+ timeout)) {//到时间点(当前时间偏移100ms)、拿锁成功
                std::cout << "in...()拿锁成功,插入元素" << i <<"成功"<< "\r\n";
                msgRecvQueue.push_back(i);
                tim_mtx.unlock();
            }
            else {//100ms内上锁未成功,超时了
                std::cout << "in...()拿锁失败,未插元素" << i << "失败" << "\r\n";
                std::chrono::milliseconds dura(100);
                std::this_thread::sleep_for(dura);
            }
        }
    }
    bool outMsgLULProc(int& command) {
            tim_mtx.lock();
            std::this_thread::sleep_for(std::chrono::milliseconds(200));
            if (!msgRecvQueue.empty()) {
                command = msgRecvQueue.front();
                //执行相应操作....
                msgRecvQueue.pop_front();
                tim_mtx.unlock();
                return true;
            }
            tim_mtx.unlock();
        return false;
    }
    //线程2:把数据从消息列表中取出的线程(读,删)
    void outMsgRecvQueue() {
        int command = 0;
        for (int i = 0; i < 10000; ++i) {
            bool ret = outMsgLULProc(command);
            if (ret == true) {//链表非空->取元素成功
                std::cout << "out...()执行,取出元素" << command << std::endl;
            }
            else {//链表空->取元素失败
                std::cout << "out...()执行,但目前消息队列为空"<< std::endl;
            }
        }
    }
private:
    std::timed_mutex tim_mtx;//时间式互斥量
    std::list msgRecvQueue;//链表(消息队列),专门用于代表玩家发来的命令
};
int main()
{
    std::cout << "The main start,thread_id = " << std::this_thread::get_id() << "\r\n";
    //--------------------------------//
    A myobj;
    std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobj);
    std::thread myInMsgObj(&A::inMsgRecvQueue, &myobj);
    myOutnMsgObj.join();
    myInMsgObj.join();
    //--------------------------------//
    std::cout << "The main end,thread_id = " << std::this_thread::get_id() << "\r\n";
    return 0;
}

3.5、互斥量的死锁

有两个锁A、B:

线程1的执行流程: =>锁A=>锁B=>执行代码=>释放锁

线程1的执行流程: =>锁B=>锁A=>执行代码=>释放锁

结果:两个线程都在无限期的等待对方释放锁,导致死锁。

示例一:>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#include 
#include 
#include 
#include 
#include 
//共享数据的保护案例<生产-消费模式>
//网络游戏服务器:
//     两个自己创建的线程,一个线程收集玩家发来的命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列中。
//     另一个线程从队列中去除玩家发来的命令,解析,然后执行相应的动作 。
//数据结构:vector,list。
class A {
public:
    //线程1:接受到玩家命令,插入到一个队列中(写)
    void inMsgRecvQueue() {
        for (int i = 0; i < 10000; ++i) {
            std::cout << "inMsgRecvQueue()执行,插入一个元素" << i << std::endl;
            mtx1.lock();
            mtx2.lock();
                msgRecvQueue.push_back(i); //i就是收到的命令
            mtx1.unlock();
            mtx2.unlock();
        }
    }
    bool outMsgLULProc(int& command) {
        //std::lock_guard sbguard(mtx);//模板类,在析构的时候会调用mtx.unlock()
        //可以智能释放锁,但是没有mtx.lock();和mtx.unlock();灵活(具体用法可以百度)
        mtx2.lock();
        mtx1.lock();
        if (!msgRecvQueue.empty()) {
            command = msgRecvQueue.front();

            msgRecvQueue.pop_front();
            mtx1.unlock();
            mtx2.unlock();
            return true;
        }
        mtx1.unlock();
        mtx2.unlock();
        return false;
    }
    //线程2:把数据从消息列表中取出的线程(读,删)
    void outMsgRecvQueue() {
        int command = 0;
        for (int i = 0; i < 10000; ++i) {
            bool ret = outMsgLULProc(command);
            if (ret == true) {//链表非空->取元素成功
                std::cout << "outMsgRecvQueue()执行,取出一个元素" << command << std::endl;
            }
            else {//链表空->取元素失败
                std::cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << std::endl;
            }
        }
    }
private:
    std::mutex mtx1;
    std::mutex mtx2;
    std::list msgRecvQueue;//链表(消息队列),专门用于代表玩家发来的命令

};
int main()
{
    std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
    //-------------------//
    A myobj;
    std::thread myOutnMsgObj(&A::outMsgRecvQueue,&myobj);
    std::thread myInMsgObj(&A::inMsgRecvQueue, &myobj);
    myOutnMsgObj.join();
    myInMsgObj.join();
    //-------------------//
    std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
    return 0;
}
示例2:>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
std::lock_guard sbguard(mtx);//这是一个模板类,<区域锁>
对象sbguard会在构造函数时调用mtx.lock()
对象sbguard会在构造函数时调用mtx.unlock()
此类可以省去在哪个位置解锁,但是由于其释放锁是在析构时,所以不是很灵活。
但是可以使代码更整洁,其实时牺牲灵活性和效率,来保证代码可读性。

死锁解决办法1:两个线程按照相同的顺序对互斥量上锁。

线程1:
    mtx1.lock();
    mtx2.lock();
        //执行....
    mtx1.unlock();
    mtx2.unlock();
线程2:
    mtx1.lock();
    mtx2.lock();
        //执行....
    mtx1.unlock();
    mtx2.unlock();

死锁解决办法2:使用std::lock()让程序自动决定上锁顺序。

线程1:
    std::lock(mtx1,mtx2);
        //执行....
    mtx1.unlock();
    mtx2.unlock();
线程2:
   std::lock(mtx1,mtx2);
        //执行....
    mtx1.unlock();
    mtx2.unlock();

死锁解决办法3:当程序的分支过多时unlock容易误写,使用模板类解决。

#include 
#include 
#include 
#include 
#include 
//共享数据的保护案例<生产-消费模式>
//网络游戏服务器:
//     两个自己创建的线程,一个线程收集玩家发来的命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列中。
//     另一个线程从队列中去除玩家发来的命令,解析,然后执行相应的动作 。
//数据结构:vector,list。
class A {
public:
    //线程1:接受到玩家命令,插入到一个队列中(写)
    void inMsgRecvQueue() {
        for (int i = 0; i < 10000; ++i) {
            std::cout << "inMsgRecvQueue()执行,插入一个元素" << i << std::endl;
            std::lock(mtx1,mtx2);
            std::lock_guard sbguard1(mtx1,std::adopt_lock);//std::adopt_lock构造函数执行时,不调用lock
            std::lock_guard sbguard2(mtx2,std::adopt_lock);
                msgRecvQueue.push_back(i); //i就是收到的命令
            //mtx1.unlock();
            //mtx2.unlock();
        }
    }
    bool outMsgLULProc(int& command) {
        //std::lock_guard sbguard(mtx);//模板类,在析构的时候会调用mtx.unlock()
        //可以智能释放锁,但是没有mtx.lock();和mtx.unlock();灵活(具体用法可以百度)
        std::lock(mtx1, mtx2);
        std::lock_guard sbguard1(mtx1, std::adopt_lock);
        std::lock_guard sbguard2(mtx2, std::adopt_lock);
        if (!msgRecvQueue.empty()) {
            command = msgRecvQueue.front();

            msgRecvQueue.pop_front();
            //mtx1.unlock();
            //mtx2.unlock();
            return true;
        }
        //mtx1.unlock();
        //mtx2.unlock();
        return false;
    }
    //线程2:把数据从消息列表中取出的线程(读,删)
    void outMsgRecvQueue() {
        int command = 0;
        for (int i = 0; i < 10000; ++i) {
            bool ret = outMsgLULProc(command);
            if (ret == true) {//链表非空->取元素成功
                std::cout << "outMsgRecvQueue()执行,取出一个元素" << command << std::endl;
            }
            else {//链表空->取元素失败
                std::cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << std::endl;
            }
        }
    }
private:
    std::mutex mtx1;
    std::mutex mtx2;
    std::list msgRecvQueue;//链表(消息队列),专门用于代表玩家发来的命令

};
int main()
{
    std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
    //--------------------------//
    A myobj;
    std::thread myOutnMsgObj(&A::outMsgRecvQueue,&myobj);
    std::thread myInMsgObj(&A::inMsgRecvQueue, &myobj);
    myOutnMsgObj.join();
    myInMsgObj.join();
    //--------------------------//
    std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
    return 0;
}

总结:std::lock(mtx1, mtx2); //一次锁定多个互斥量,并自动决定上锁顺序(很少用)
           std::lock_guard sbguard1(mtx1, std::adopt_lock);//自动解锁
           std::lock_guard sbguard2(mtx2, std::adopt_lock);//自动解锁
建议:还是使用mtx.lock();、mtx.unlock();的形式。

3.6、独占锁的使用

        互斥锁保证了线程间的同步,但是却将并行操作变成了串行操作,这对性能有很大的影响,所以我们要尽可能的减小锁定的区域,也就是使用细粒度锁
std::lock_guard:区域锁
std::unique_lock:独占式锁
两者具体区别和优缺点可以自行百度,unique_lock可以完全取代区域锁,但是有缺点。

用法一、lock_guard与unique_lock的替换
    std::lock_guard sbguard(mtx);
        可以替换为.....
    std::unique_lock sbguard(mtx);
/*==================================================================*/
用法二、lock_guard与unique_lock的替换
    mtx.lock();
    std::lock_guard sbguard(mtx,std::adopt_lock);
        可以替换为.....
    mtx.lock();
    std::unique_lock sbguard(mtx, std::adopt_lock);
    //注意:std::adopt_lock:代表传入的互斥量已经被加锁了
/*==================================================================*/
用法三、引出问题
    线程1:
        mtx.lock();
        std::lock_guard sbguard(mtx,std::adopt_lock);
    线程2:
        mtx.lock();
        std::lock_guard sbguard(mtx,std::adopt_lock);
        std::this_thread::sleep_for(std::chrono::milliseconds(5000));
    //注意:线程2的延时5s,不快速释放锁,导致线程1的上锁阻塞了5s
    //解决办法,线程1先尝试上锁,上锁不成功,立即返回,不阻塞。<见用法四>
/*==================================================================*/
用法四、使用try_to_lock形参=>尝试上锁
    线程1:
        std::unique_lock sbguard(mtx, std::try_to_lock);//尝试加锁
        if (sbguard.owns_lock()) {//加锁成功
            //操作共享数据...
        }
        else {//加锁失败
            //处理其他的事...
        }   
    线程2:    
        mtx.lock();//上锁
        std::unique_lock sbguard(mtx,std::adopt_lock);//区域锁
        std::this_thread::sleep_for(std::chrono::milliseconds(10000));//延时10s
        //操作共享资源...
    //注意:使用try_to_lock时,自己不能提前去加锁,否则会卡死  
/*==================================================================*/  
用法五、使用defer_lock形参
    线程1:
        std::unique_lock sbguard(mtx, std::defer_lock);//绑定一个未初始化的锁
        sbguard.lock();//需要手动初始化
            //操作共享资源...
    线程2:    
        mtx.lock();//上锁
            //操作共享资源...
        mtx.unlock();//解锁
    //注意:这些类模板的作用,都是绑定互斥量,在原来互斥量的基础上加了一些方法而已
    //比如类模板会在对象析构时对传入的互斥量解锁等等,也能通过对象手动的对传入的互斥量上锁解锁等
/*==================================================================*/
用法六、调用方法try_lock(),用法类似形参try_to_lock
    //在用法五的基础上修改
    线程1:
        std::unique_lock sbguard(mtx,std::defer_lock);
        if (sbguard.try_lock() == true) {
            //操作共享资源...
        }
        else {
            //处理其他的事...
        }
     线程2:    
        mtx.lock();//上锁
            //操作共享资源...
        mtx.unlock();//解锁
     //注意:不用模板类绑定互斥量,原互斥量也可以使用此方法(注意使用unlock())
/*==================================================================*/
用法七、调用方法release(),返回模板类对象绑定的mutex指针,<解除unique_lock与mutex的绑定>
    //如果mutex已经加锁,接管过来之后,还是加锁状态
    线程1:
        std::unique_lock sbguard(mtx);//sbguard与mtx绑定
        std::mutex* p_mtx = sbguard.release();//sbguard与mtx分离
            //操作共享资源...
        p_mtx->unlock();
    线程2:    
        mtx.lock();//上锁
            //操作共享资源...
        mtx.unlock();//解锁
/*==================================================================*/
说明1:std::adopt_lock:标识传入的互斥量已经上锁了。
说明2:std::defer_lock:标识传入的互斥量还没有上锁。
说明3:std::try_to_lock:标识对传入的互斥量尝试上锁,需要手工加锁。
说明4:lock()和unlock()之间的代码的段的大小,叫做锁的粒度,粒度越细越好。
说明5:同一个mutex不能绑定两个模板类对象。
说明6:换绑定,可以使用std::move将一个unique_lock对象里的mutex掏出来与另一个绑定在一起。
说明7:std::unique_lock类型的函数,返回值时,调用的是移动拷贝构造函数。

3.7、单例模式的介绍

<前言:单例模式只是设计模式的一种,具体的其他模式还是需要学习的,因为是C++的知识,所以其他的设计模式,可以通过C++书籍或者百度等工具自行学习,这里只说单例模式>

所谓单例模式,就是设定一种类,其只能实例化一个对象,创建代码如下:

#include 
#include 
#include 
#include 
#include 
//共享数据的保护案例<生产-消费模式>
//网络游戏服务器:
//     两个自己创建的线程,一个线程收集玩家发来的命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列中。
//     另一个线程从队列中去除玩家发来的命令,解析,然后执行相应的动作 。
//数据结构:vector,list。
class MyCAS {
public:
    static MyCAS* GetInstance() {
        if (m_instance == NULL) {
            m_instance = new MyCAS();
            static CGarhuishou cl;
        }
        return m_instance;
    }
    class CGarhuishou {//类中套类,用来释放对象
    public:
        ~CGarhuishou() {
            if (MyCAS::m_instance) {
                delete MyCAS::m_instance;
                MyCAS::m_instance = NULL;
            }
        }
    };
    void prt() {
        std::cout << "hello" << std::endl;
    }
private:
    MyCAS() {//私有化构造函数
        std::cout << "构造执行" << std::endl;
    }
    ~MyCAS() {//私有析构函数
        std::cout << "析构执行" << std::endl;
    }
    static MyCAS* m_instance;//静态成员变量
};
MyCAS* MyCAS::m_instance = NULL;//静态成员初始化
int main()
{
    MyCAS* cls = MyCAS::GetInstance();  //cls与cls1指向的是同一个对象(只能实例化一个对象)
    MyCAS* cls1 = MyCAS::GetInstance();
    return 0;
}

多个线程中实例化单例类,会导致共享资源冲突,产生两个对象的产生,需要做如下保护:

#include 
#include 
#include 
#include 
#include 
//共享数据的保护案例<生产-消费模式>
//网络游戏服务器:
//     两个自己创建的线程,一个线程收集玩家发来的命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列中。
//     另一个线程从队列中去除玩家发来的命令,解析,然后执行相应的动作 。
//数据结构:vector,list。
std::mutex resource_mutex;
class MyCAS {
public:
    //GetInstance()函数是被频繁被调用的,而m_instance的冲突只会发生在第一次创建的时候,后续不需要再互斥了
    //当第一次创建完成后,之后如果再调用GetInstance()都加锁的话,效率会低,故使用双重加锁模式,只在第一次创建时使用锁
    static MyCAS* GetInstance() {
        if (m_instance == NULL){//双重锁定<第一重>
            std::unique_lock mymutex(resource_mutex);//
            if (m_instance == NULL) {//双重锁定<第二重>
                m_instance = new MyCAS();
                static CGarhuishou cl;
            }
        }
        return m_instance;
    }
    class CGarhuishou {//类中套类,用来释放对象
    public:
        ~CGarhuishou() {
            if (MyCAS::m_instance) {
                delete MyCAS::m_instance;
                MyCAS::m_instance = NULL;
            }
        }
    };
    void prt() {
        std::cout << "hello" << std::endl;
    }
private:
    MyCAS() {//私有化构造函数
        std::cout << "构造执行" << std::endl;
    }
    ~MyCAS() {//私有析构函数
        std::cout << "析构执行" << std::endl;
    }
    static MyCAS* m_instance;//静态成员变量
};
MyCAS* MyCAS::m_instance = NULL;//静态成员初始化

void mythread(){//线程入口函数
    std::cout << "我的线程开始执行了" << std::endl;
    MyCAS* pa = MyCAS::GetInstance();
    std::cout << "我的线程执行完毕了" << std::endl;
}
int main()
{   
    std::thread mytobj1(mythread);//线程1
    std::thread mytobj2(mythread);//线程2
    mytobj1.join();
    mytobj2.join();
    return 0;
}

std::call_once():C++11引入,第二的形参是一个函数名,作用是保证传入的函数只被调用一次。
=>std::call_once()具备互斥的能力,而且效率上比互斥量消耗的资源更少,需要与一个标记std::once_flag结合使用,std::once_flag是一个结构,通过std::once_flag决定函数是否执行,函数调用成功后,std::once_flag会更改为另一个状态。示例代码如下:

std::once_flag g_flag;//定义一个未被置位的标记
class MyCAS {
private: 
    static void CreateInstance() {//只会被执行一次的函数
        m_instance = new MyCAS();
        static CGarhuishou cl;
    }
public:
    //GetInstance()函数是被频繁被调用的,而m_instance的冲突只会发生在第一次创建的时候,后续不需要再互斥了
    //当第一次创建完成后,之后如果再调用GetInstance()都加锁的话,效率会低,故使用双重加锁模式,只在第一次创建时使用锁
    static MyCAS* GetInstance() {
        //if (m_instance == NULL){//双重锁定<第一重>
        //    std::unique_lock mymutex(resource_mutex);//
        //    if (m_instance == NULL) {//双重锁定<第二重>
        //        m_instance = new MyCAS();
        //        static CGarhuishou cl;
        //    }
        //}
        std::call_once(g_flag,CreateInstance);
        return m_instance;
    }
    class CGarhuishou {//类中套类,用来释放对象
    public:
        ~CGarhuishou() {
            if (MyCAS::m_instance) {
                delete MyCAS::m_instance;
                MyCAS::m_instance = NULL;
            }
        }
    };
    void prt() {
        std::cout << "hello" << std::endl;
    }
private:
    MyCAS() {//私有化构造函数
        std::cout << "构造执行" << std::endl;
    }
    ~MyCAS() {//私有析构函数
        std::cout << "析构执行" << std::endl;
    }
    static MyCAS* m_instance;//静态成员变量
};
MyCAS* MyCAS::m_instance = NULL;//静态成员初始化
void mythread(){//线程入口函数
    std::cout << "我的线程开始执行了" << std::endl;
    MyCAS* pa = MyCAS::GetInstance();
    std::cout << "我的线程执行完毕了" << std::endl;
}
int main()
{   
    std::thread mytobj1(mythread);//线程1
    std::thread mytobj2(mythread);//线程2
    mytobj1.join();
    mytobj2.join();
    return 0;
}
//说明1:std::call_once()类似于双重锁定保证函数只被执行一次,实现的机制还是互斥。
//说明2:std::call_once()的效率比直接互斥高,比双重锁定的互斥略低一点。

3.8、std::wait()的使用

条件变量

<前言:以5.4中的程序为例,取出消息的线程中,循环检测队列中是否有消息,当没有消息时,线程再空转,实在是一种损耗。为了优化这个问题,让插入消息的线程通知取消息的线程就行了,没有收到消息,让其睡眠,有点像QT中的信号与槽机制>

std::condition_variable是一个模板类,其与互斥量组合使用。
std::unique_lock+std::mutex+std::condition_variable+wait()+notify_one()/notify_all()的使用,示例代码如下:

#include 
#include 
#include 
#include 
#include 
//共享数据的保护案例<生产-消费模式>
//网络游戏服务器:
//     两个自己创建的线程,一个线程收集玩家发来的命令(用一个数字代表玩家发来的命令),并把命令数据写到一个队列中。
//     另一个线程从队列中去除玩家发来的命令,解析,然后执行相应的动作 。
//数据结构:vector,list。
class A {
public:
    //线程1:接受到玩家命令,插入到一个队列中(写)
    void inMsgRecvQueue() {
        for (int i = 0; i < 10000; ++i) {
            std::cout << "inMsgRecvQueue()执行,插入一个元素" << i << std::endl;
            std::unique_lock sbguard(mtx);//sbguard与mtx绑定
            msgRecvQueue.push_back(i); //再末尾插入一个元素,i就是收到的命令
            my_cond.notify_one();//尝试唤醒调用my_cond.wait()的那个线程
        }
    }
    void outMsgRecvQueue() {
        int command = 0;
        while (true) {
            std::unique_lock sbguard(mtx);
            //wait()用来等一个东西
            //如果第二的参数即lambda表达式返回值是true,直接返回。
            //如果第二的参数即lambda表达式返回值是false,wait将解锁互斥量,并阻塞到本行,直到其他的某个线程调用notify_one()将其唤醒。
            //如果wait()没有第二个参数,wait将解锁互斥量,并直接阻塞到本行,并等待其他的某个线程调用notify_one()将其唤醒。

            //没有第二参数时,当wait()被唤醒时,它首先会不断的尝试重新获取互斥量,直到获取到之后,跳出wait(),往下执行。
            //当有第二参数时,当wait()被唤醒时,它首先会不断的尝试重新获取互斥量,直到获取到之后,执行lambda表达式...
            //...如果lambda表达式返回false=>解锁,睡眠,如果lambda表达式返回true,跳出wait(),往下执行
            my_cond.wait(sbguard, [this]{
                if (!msgRecvQueue.empty())
                    return true;
                return false;
            });
            command = msgRecvQueue.front();
            msgRecvQueue.pop_front();
            sbguard.unlock();//由于unique_lock的灵活性,可以提前解锁
            std::cout << "outMsgRecvQueue()执行,取出一个元素" << command << std::endl; 
        }
    }
private:
    std::list msgRecvQueue;//链表(消息队列),专门用于代表玩家发来的命令
    std::mutex mtx;//生成一个互斥量
    std::condition_variable my_cond;//生成一个条件变量对象

};
int main()
{   
    A myobj;
    std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobj);
    std::thread myInMsgObj(&A::inMsgRecvQueue, &myobj);
    myOutnMsgObj.join();
    myInMsgObj.join();
    return 0;
}
//说明1:线程1执行完毕退出会导致线程2一直卡在那进行死等,这个是有缺陷的,但是这个缺陷很容易解决
    比如:规定一个command是退出命令,线程1在退出时插入这个退出命令并notify_one(),并线程二收到这个命令就退出循环。这样线程2就不会死等了。<解决死等方法很多,所以不是个事>
//说明2:如过第二个线程没有阻塞到wait(),线程1调用notify_one()其实是失效的,因为线程本身是唤醒的。<也可叫虚假唤醒>
//说明3:如果有如下两个线程,notify_one()一次只能唤醒其中的一个
    std::thread myOutnMsgObj1(&A::outMsgRecvQueue, &myobj); //这两个线程用的一个函数
    std::thread myOutnMsgObj2(&A::outMsgRecvQueue, &myobj);
//说明4:如果有如下两个线程,你想唤醒一次唤醒两个,可以用notify_all()
    std::thread myOutnMsgObj1(&A::outMsgRecvQueue, &myobj);
    std::thread myOutnMsgObj2(&A::outMsgRecvQueue, &myobj);

3.9、std::async()创建异步线程

<1>、使用std::async()创建异步线程/任务:

//std::async、std::future:创建后台任务并返回值
//希望线程返回一个结果
//std::async 是个函数模板,用来启动一个异步任务,启动后返回一个std::future对象,std::future是个类模板。
//启动异步任务:自动创建一个线程并开始执行对应的县城入口函数,它返回一个std::future对象,
//.............这个std::future对象里边就含有县城入口函数所返回的结果(线程返回的结果),我们可以通过调用future对象的成员函数get()来获取结果。
//.............std::future提供了一种访问异步操作结果的机制,即这个结果你不能马上拿到,但在将来可以拿到。
//*************************************************************************************//
示例一:普通函数做std::async()的参数
#include 
#include 
#include 
#include 
#include 
#include 
int mythread() {
    std::cout << "mythread() Start th_id = " < 
  
//示例1:把线程函数封装到std::packaged_task的对象中
#include 
#include 
#include 
#include 
#include 
#include 
//std::packaged_task:打包任务,把任务包装起来,方便将来作为线程入口函数。
//std::packaged_task是个类模板,他的模板参数是各种可调用对象。
int mythread(int mypar) {
    std::cout << "=" << mypar << std::endl;
    std::cout << "mythread() Start th_id = " << std::this_thread::get_id() << std::endl;
    std::chrono::milliseconds dura(5000);//延时5s
    std::this_thread::sleep_for(dura);
    std::cout << "mythread() Finish th_id = " << std::this_thread::get_id() << std::endl;
    return 5;
}
int main()
{   
    //把线程函数与一个<对象/变量>绑定,用操作<对象/变量>的方式使用线程函数,也算是符合万物皆是对象的概念
    std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
    std::packaged_task mypt(mythread);//把线程函数包装到一个packaged_task对象中
    std::thread t1(std::ref(mypt),1);
    t1.join();
    std::future result = mypt.get_future();
    std::cout << "result = " << result.get() << std::endl;
    return 0;
}
/***************************************************************************************/
//示例2:把lambda表达式封装到std::packaged_task的对象中
#include 
#include 
#include 
#include 
#include 
#include 
int main()
{   
    std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
    std::packaged_task mypt([](int mypar) {
        std::cout << "=" << mypar << std::endl;
        std::cout << "mythread() Start th_id = " << std::this_thread::get_id() << std::endl;
        std::chrono::milliseconds dura(5000);//延时5s
        std::this_thread::sleep_for(dura);
        std::cout << "mythread() Finish th_id = " << std::this_thread::get_id() << std::endl;
        return 5;
    });
    std::thread t1(std::ref(mypt),1);
    t1.join();
    std::future result = mypt.get_future();
    std::cout << "result = " << result.get() << std::endl;
    return 0;
}
/***************************************************************************************/
示例3:std::packaged_task的对象也可以直接被调用
#include 
#include 
#include 
#include 
#include 
#include 
int main()
{   
    std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
    std::packaged_task mypt([](int mypar) {
        std::cout << "=" << mypar << std::endl;
        std::cout << "mythread() Start th_id = " << std::this_thread::get_id() << std::endl;
        std::chrono::milliseconds dura(5000);//延时5s
        std::this_thread::sleep_for(dura);
        std::cout << "mythread() Finish th_id = " << std::this_thread::get_id() << std::endl;
        return 5;
    });
    mypt(105); //std::packaged_task对象也可以直接被调用(相当于函数),不创建线程
    std::future result = mypt.get_future();
    std::cout << "result = " << result.get() << std::endl;
    return 0;
}
/***************************************************************************************/
示例4:把std::packaged_task对象放入容器中
#include 
#include 
#include 
#include 
#include 
#include 
std::vector < std::packaged_task > mytasks; // 定义一个容器
int main()
{   
    std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
    std::packaged_task mypt([](int mypar) {
        std::cout << "=" << mypar << std::endl;
        std::cout << "mythread() Start th_id = " << std::this_thread::get_id() << std::endl;
        std::chrono::milliseconds dura(5000);//延时5s
        std::this_thread::sleep_for(dura);
        std::cout << "mythread() Finish th_id = " << std::this_thread::get_id() << std::endl;
        return 5;
    });
    //把对象移动到容器mytasks中
    mytasks.push_back(std::move(mypt));
    //从容器中mytasks取出
    std::packaged_task mypt2;
    auto iter = mytasks.begin();
    mypt2 = std::move(*iter);
    mytasks.erase(iter);//删除iter指向的元素,此时iter失效,后续不可以再使用iter
    //执行
    mypt2(521);
    std::future result = mypt2.get_future();
    std::cout << "result = " << result.get() << std::endl;
    return 0;
}
/***************************************************************************************/

3.11、std::promise<>的使用

一个线程等待获取另一个线程的计算结果:

#include 
#include 
#include 
#include 
#include 
#include 
//std::promise,类模板,可实现在某个线程中给其赋值,在其他线程中读它。
void mythread(std::promise &tmpp,int calc) {
    //做一系列复杂的操作
    calc++;
    calc *= 10;
    std::chrono::milliseconds dura(5000);//延时5s
    std::this_thread::sleep_for(dura);
    //计算出结果了
    int result = calc;//读取结果
    tmpp.set_value(result);//把结果保存到tmpp中
    return;
}
void mythread2(std::future& tmpf,std::thread &th) {
    auto result = tmpf.get();
    std::cout << "result = " << result << ",id=" << std::this_thread::get_id() << std::endl;
    th.join();
    return;
}
int main()
{   
    std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
    //线程1
    std::promise myprom;
    std::thread th(mythread, std::ref(myprom),1);//创建线程1
    std::future ful = myprom.get_future();//promise和future绑定,用于获取线程返回值
    //线程2
    std::thread th2(mythread2,std::ref(ful),std::ref(th));//创建线程2,在线程2中等待线程1的结果

    th2.join();
    return 0;
}

3.12、std::future<>的wait_for()方法

一个线程获取另一个线程的运算结果,必须等另一个线程执行完才能获取到,现实情况中,由于系统是非实时的,所以其每次执行完毕的所耗时间是不定的、浮动的,如果线程一直迟迟运行不完,我们也不能一直等待(等待是阻塞的),所以用wait_for()方法,每等待相应的时间就判断一下对方的状态,这样更灵活的等待对方的结果。也可以在对方是否超时做出相应处理。示例代码如下:

//示例:以std::async()为例
#include 
#include 
#include 
#include 
#include 
#include 
int mythread() {
    std::cout << "mythread() Start th_id = " << std::this_thread::get_id() << std::endl;
    std::chrono::milliseconds dura(5000);//延时5s
    std::this_thread::sleep_for(dura);
    std::cout << "mythread() Finish th_id = " << std::this_thread::get_id() << std::endl;
    return 5;
}
int main()
{
    std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
    std::future result = std::async(mythread);//创建一个线程并开始执行(result与线程绑在了一起)
    std::cout << "continue...!" << std::endl;
    int i = 0;
    while (true) {
        std::future_status status = result.wait_for(std::chrono::seconds(1));//周期1s唤醒自己,查看一次对方是否执行完毕
        std::cout << "第" <<++i<<"次唤醒:";
        if (status == std::future_status::ready) {
            std::cout << "线程执行完毕=>";
            std::cout << " result.get()= " << result.get() << std::endl;
            break;
        }
        else if (status == std::future_status::timeout) {
            std::cout << "线程未执行完毕" << std::endl;
        }
        else if (status == std::future_status::deferred) {
            std::cout << "线程被延迟执行" << std::endl;
        }
    }
    return 0;
}

3.13、std::shared_future<>的使用

在前面的历程中我们知道std::future<> 的get()方法只能被调用一次,因为其是一个移动语义,如果移动两次,肯定会出问题,这就限制了只能由一个线程中调用get()去获取某个线程的计算结果,为了解决这个限制(即多个线程都要获取某个线程的结果),引入了std::shared_future<>,代码如下:

//std::shared_future<>的get()方法使用的不是移动语义,而是复制语义,故可以get()两次
#include 
#include 
#include 
#include 
#include 
#include 
void mythread(std::promise& tmpp, int calc) {
    //做一系列复杂的操作
    calc++;
    calc *= 10;
    std::chrono::milliseconds dura(5000);//延时5s
    std::this_thread::sleep_for(dura);
    //计算出结果了
    int result = calc;//读取结果
    tmpp.set_value(result);//把结果保存到tmpp中
    return;
}
void mythread2(std::shared_future& tmpf, std::thread& th) {
    auto result = tmpf.get();//get()只能被调用一次,因为其是个移动语义,所以不能移动第二次
    std::cout << "result = " << result << ",id=" << std::this_thread::get_id() << std::endl;
    //printf("result=%d,th_id=%d\r\n", result, std::this_thread::get_id());
    if (th.joinable()){th.join();} 
}
void mythread3(std::shared_future& tmpf, std::thread& th) {
    auto result = tmpf.get();//get()只能被调用一次,因为其是个移动语义,所以不能移动第二次
    std::cout << "result = " << result << ",id=" << std::this_thread::get_id() << std::endl;
    //printf("result=%d,th_id=%d\r\n", result, std::this_thread::get_id());
    if (th.joinable()) { th.join(); }
}
int main()
{
    std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
    //线程1
    std::promise myprom;
    std::thread th(mythread, std::ref(myprom), 1);//创建线程1
    std::shared_future sful = myprom.get_future();//promise和future绑定,用于获取线程返回值
    //线程2、3
    std::thread th2(mythread2, std::ref(sful), std::ref(th));//创建线程2,在线程2中等待线程1的结果
    std::thread th3(mythread3, std::ref(sful), std::ref(th));//创建线程3,在线程3中等待线程1的结果
    //join线程2、3
    th2.join(); th3.join();
    return 0;
}
//说明1:std::future也可以转为std::shared_future,示例如下
//       std::future ful = myprom.get_future();
//       std::shared_future sfulx(ful.share());//ful.share()也是移动语义
//说明2:std::future和std::shared_future的valid()方法可以判断其对象里的值是否有效
//       例如:if(sful.valid()){  }//判断sful变量是否被std::move()移动过

3.14、std::atomic<>创建原子对象

//示例1:整形原子操作
#include 
#include 
#include 
#include 
#include 
#include 
//原子操作:std::atomic(类模板)、不可分割的操作 <针对简单的操作>
//互斥量:多线程中,保护共享数据:锁=>保护共享数据=>开锁 <针对复杂的操作>
/*
 * 原子1:使用原子汇编码,代码粒度小于中断的的代码。
 * 原子2:使用开关中断,暂时屏蔽中断从而不让系统调度,就可以实现原子性。
 * 以上是两种原子性。这样的代码的执行叫原子操作,它是一种无锁技术,效率上比互斥量好。
 * 但原子操作是很简单的,能力一般。而互斥量可以互斥整个代码段,所以各有优缺点。 
 */
std::atomic g_mycount = 0; //定义一个int原子对象
void mythread() {
    for ( int i = 0;i < 1000000;i++){
        g_mycount++; //原子操作
        //g_mycount+=1; //原子操作
        //g_mycount=g_mycount+1;//不是原子操作
        //结论:不是所有的操作符都是原子性的,一般针对++、--、+=、&=、|=、^=是原子性的
}
int main()
{
    std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
    std::thread th1(mythread);//创建线程1
    std::thread th2(mythread);//创建线程2
    th1.join(); th2.join();
    std::cout << "g_mycount=" << g_mycount << std::endl;
    return 0;
}
//*************************************************************************************//
//示例2:布尔型原子操作
#include 
#include 
#include 
#include 
#include 
#include 
std::atomic g_ifend = true;//定义一个bool原子对象
void mythread() {
    std::chrono::milliseconds dura(1000);
    while (g_ifend) {//原子操作
        std::cout << "thread_id = " << std::this_thread::get_id() << "运行中" << std::endl;
        std::this_thread::sleep_for(dura);
    }
    std::cout << "thread_id = " << std::this_thread::get_id() << "运行结束" << std::endl;
}

int main()
{
    std::cout << "main() Start th_id = " << std::this_thread::get_id() << std::endl;
    std::thread th1(mythread);//创建线程1
    std::thread th2(mythread);//创建线程2
    std::chrono::milliseconds dura(5000);//主线程休息5s
    std::this_thread::sleep_for(dura);
    g_ifend=false;//原子操作、让线程退出循环
    th1.join(); th2.join();
    std::cout << "主线程结束"<< std::endl;
    return 0;
}
//说明1:原子操作项目中一般不常用。
//说明2:一般用于计数,比如数据包的统计。
//说明3:代码一定要稳定,给公司写项目时一定要用自己拿得准的的代码(稳定首位)。
//*************************************************************************************//

3.15、Windows临界区的简单使用

临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
互斥量:为协调共同对一个共享资源的单独访问而设计的。
信号量:为控制一个具有有限数量用户资源而设计。
事    件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

/***************************************************************************************/
//示例代码1:临界区的简单使用
#include 
#include 
#include 
#include 
#include 

#include //不属于C++库,此库很大,一般使用预编译
#define __WINDOWSJQ_
//windows临界区           
class A {
public:
    //线程1:接受到玩家命令,插入到一个队列中(写)
    void inMsgRecvQueue() {
        for (int i = 0; i < 10000; ++i) {
            std::cout << "inMsgRecvQueue()执行,插入一个元素" << i << std::endl;
            #ifdef __WINDOWSJQ_ 
                EnterCriticalSection(&my_winsec);//进入临界区
                msgRecvQueue.push_back(i); //i就是收到的命令
                LeaveCriticalSection(&my_winsec);//离开临界区
            #else
                mtx.lock();
                msgRecvQueue.push_back(i); //i就是收到的命令
                mtx.unlock();
            #endif
        }
    }
    bool outMsgLULProc(int& command) {
        #ifdef __WINDOWSJQ_ 
            EnterCriticalSection(&my_winsec);//进入临界区
            if (!msgRecvQueue.empty()) {
                command = msgRecvQueue.front();
                msgRecvQueue.pop_front();
                LeaveCriticalSection(&my_winsec);//离开临界区
                return true;
            }
            LeaveCriticalSection(&my_winsec);//离开临界区
        #else
            mtx.lock();
            if (!msgRecvQueue.empty()) {
                command = msgRecvQueue.front();

                msgRecvQueue.pop_front();
                mtx.unlock();
                return true;
            }
            mtx.unlock();
        #endif
        return false;
    }
    //线程2:把数据从消息列表中取出的线程(读,删)
    void outMsgRecvQueue() {
        int command = 0;
        for (int i = 0; i < 10000; ++i) {
            bool ret = outMsgLULProc(command);
            if (ret == true) {//链表非空->取元素成功
                std::cout << "outMsgRecvQueue()执行,取出一个元素" << command << std::endl;
            }
            else {//链表空->取元素失败
                std::cout << "outMsgRecvQueue()执行,但目前消息队列为空" << i << std::endl;
            }
        }
    }
    //构造函数
    A() {
        #ifdef __WINDOWSJQ_ 
            InitializeCriticalSection(&my_winsec);//初始化临界区
        #endif
    }
private:
    std::mutex mtx;
    std::list msgRecvQueue;//链表(消息队列),专门用于代表玩家发来的命令
    //------创建windows临界区
    #ifdef __WINDOWSJQ_ //windows中的临界区,非常类似于C++11中的mutex
        CRITICAL_SECTION my_winsec;//临界区必须初始化,在构造函数中初始化
    #endif
};
int main()
{
    std::cout << "The main thread start and thread_id = " << std::this_thread::get_id() << "\r\n";
    //--------------------------------//
    A myobj;
    std::thread myOutnMsgObj(&A::outMsgRecvQueue, &myobj);
    std::thread myInMsgObj(&A::inMsgRecvQueue, &myobj);
    myOutnMsgObj.join();
    myInMsgObj.join();
    //--------------------------------//
    std::cout << "The main thread end and thread_id = " << std::this_thread::get_id() << "\r\n";
    return 0;
}
/***************************************************************************************/
//示例代码2:临界区的的多次进入
//windows的同一个线程中,相同的临界区变量,可以进入多次,但也离开相应的次数
{
    EnterCriticalSection(&my_winsec);//进入临界区
    EnterCriticalSection(&my_winsec);//进入临界区
    //执行相应代码......
    LeaveCriticalSection(&my_winsec);//离开临界区
    LeaveCriticalSection(&my_winsec);//离开临界区
}
//说明1:C++11的互斥量中,相同的变量在同一个线程中不可以锁两次
//说明2:像智能指针,区域锁等这种自动释放的类,叫做RAII类,资源获取即初始化,其主要实现机理
//.......就是在构造中初始化资源,在析构中释放资源。
/***************************************************************************************/

四、线程类的封装模板参考

4.1、线程封装前言

更新中。。。

五、线程池类的封装模板参考

5.1、线程池封装前言

服务器-客户端程序,每来一个客户端,服务器端创建一个线程提供服务。
客户端少没有问题,如果一下子来上千、上万个客户端,创建线程就可能失败。
程序不稳定,编写的代码中,偶尔创建一个线程这种代码出现问题,让人不安。
为了应对上述不稳定的问题,提出了线程池的概念
<1>、线程池的概念:
        把一堆线程弄到一起,统一管理。这种统一管理调度,循环利用线程的方式叫线程池。
<2>、实现方式:
        在程序启动时,一次性的创建好一定数量的线程,要使用线程时,从线程池中抓过来一个进行使用,使用完毕后再放回到线程池中,程序运行过程中不再临时创建线程,使程序更稳定。
<3>、线程数量:
        采用某些技术开发程序,api接口供应商有时会给出数量的建议,按照指示设置数量。

更新中。。。

你可能感兴趣的:(计算机,多线程,c++11,多线程)