C++并发与多线程

一、并发、进程、线程的基本概念

1.并发

线程数大于核数,任务需要不同切换

进程:可执行程序运行起来了

线程:每个进程都有一个主线程,主线程是唯一的,

执行可执行程序,产生一个进程。就会被分配给一个线程主线程来调用这个main函数中的代码

线程并不是越多越好,每个线程,都需要一个独立的堆栈空间(1M),线程之间的切换需要保存很多中间状态,切换会消耗本来属于程序的时间

2.多进程并发

进程间通信

同一个电脑:管道、文件、消息队列、共享内存

不同电脑:socket

3.多线程并发

单个进程中,创建了多个线程。每个线程都有自己独立的运行路径,但是每一个进程中的所有线程共享空间(内存)

通信

全局变量,指针,引用,都可以在线程之间进行传递,所以使用多线程开销远小于多进程。

共享内存也带来新的问题,如数据一致性的问题;线程A,线程B

5.总结

和进程比,线程有以下优点:

  • 线程启动速度更快
  • 系统开销更小,执行速度更快。

实现更加复杂,注意数据一致性问题

优先考虑多线程。

C++新标准线程库

旧:

windows:CreateThread(),_beginthread(),__beginthreadexe()创建线程

linux:pthread_create():创建线程

临界区,互斥量

POSIX thread(pthread):跨平台;配置麻烦

总结:以往多线程代码不能跨平台;

新:

C++11标准:C++11新标准,C++本身增加对多线程的支持,可移植性增加。

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

1.使用函数创建新线程

join-阻塞

#include <iostream>
#include <thread> 
using namespace std;
void MyThread()
{
    cout << "我的线程开始了" << endl;

    cout << "我的线程结束了" << endl;
}

int main()
{
    //创建新线程 `thread`是个标准库的类
    thread mytd(MyThread);
    //阻塞主线程并等待MyThread子线程执行完毕。如果不阻塞、则子线程未执行完,但主线程提前执行完毕。此时可能会报错。
    mytd.join();
    cout << "Main proc begin" << endl;
    system("pause");
    return 0;
}

detach-分离

  • 子线程不一定能被在前端执行完
  • 一旦detach,不能再join
//主线程和子线程脱离运行
...
thread mytd(MyThread);
//使主线程和子线程脱离,由运行时库托管,主线程结束后,子线程会被转入后台运行、即子线程不一定能被在前端执行完
mytd.detach();
cout << "Main proc begin" << endl;
...

joinable-判断是否能够join或detach

thread mytd(MyThread);
mytd.detach();
if (mytd.joinable())
{
    cout << "joinabel" << endl;

}
else
{
    cout << "disable" << endl;
}

输出如下

我的线程开始了disable
Main proc begin!

2.先创建线程指针,然后赋值入口函数-反对这种做法,delete时一定会报错

#include <iostream>
#include <thread>
void mProg(int i)
{
    std::cout << i << std::endl;
}
std::thread * mtd;
int main()
{
    mtd = new std::thread(mProg, 1);
    mtd->join();
    system("pause");
    return 0;
}

delete一定会报错、不知道为什么。

#include <iostream>
#include <thread>
void whileThredProg()
{
    std::cout << "whileThredProg退出" << std::endl;
    return;
}


int main()
{
    std::thread *mtd;
    mtd = new std::thread(whileThredProg);
    delete mtd;
    mtd = NULL;
    system("pause");
    return 0;
}
Debug Error!
...
R6010
- abort() has been called
...

3.其他创建线程方法

仿函数

#include <iostream>
#include <thread> 
using namespace std;
class classA
{
public:
    void operator()(int i)
    {
        cout << "我的线程operator()开始执行了" << endl;
        cout << i << endl;
        //...
        cout << "我的线程operator()结束执行了" << endl;
    }
};
int main()
{
    classA a;
    thread mytd2(a,1);
    mytd2.join();
    cout << "Main proc begin!" << endl;
    system("pause");
    return 0;
}
仿函数需要注意
class classA
{
public:
    int & m_i;
    classA(int &i) : m_i(i) {};
    void operator()()
    {
        while (1)
        {
            cout << "let see m_i = "<<m_i << endl;
        }
    }
};

int main()
{
    int num = 3;
    classA a(num);
    thread mytd2(a);
    //mytd2.join();
    mytd2.detach();
    cout << "1. Main proc begin!" << endl;
    cout << "2. Main proc begin!" << endl;
    cout << "3. Main proc begin!" << endl;
    cout << "4. Main proc begin!" << endl;
}

子线程引用了主程序的局部变量,随着主线程结束、主线程资源释放。则子线程的引用无效。

线程资源相关

问:主线程结束,则主线程资源被释放了。主线程的中创建的局部变量对象还在吗?

答:存在、因为该对象被复制到了子线程里。是值传递的方式。(拷贝构造函数)

类非静态函数

class classA
{
    int m_i;
public:
    classA(int i):m_i(i){};
    void mytd(int j)
    {
        cout << m_i << " " << j << endl;
    }
};
int main()
{
    classA ca(1);
    std::thread mtd(&classA::mytd, ca, 2);
    mtd.join();
    system("pause");
    return 0;
}

类静态函数

#include <iostream>
#include <thread>
using namespace std;
class classA
{
public:
    static void m_tdp(int i)
    {
        cout << i << endl;
    }
};
int main()
{
    std::thread mtd(&classA::m_tdp, 2);
    mtd.join();
    system("pause");
    return 0;
}

lambda表达式

#include <iostream>
#include <thread>
using namespace std;
auto myLambdaThread = [](int i, int j){
    cout << i << endl;
    cout << j << endl;
    cout << "3" << endl;
};

int main()
{
    std::thread myLambdaThread(myLambdaThread, 1, 2);
    myLambdaThread.join();
    system("pause");
    return 0;
}

三、线程传参详解,detach()大坑,成员函数作为线程函数

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

a.传参、陷阱

#include <iostream>
#include <thread>
using namespace std;
void myPrint(const int & i, char *buff)
{
    cout << i << endl;//虽然这里定义是引用,但是线程创建时是值传递。所以主线程销毁,子线程detach后参数不会发生错误。
    cout << buff << endl;
    return;
}

int main()
{
    int mvar = 1;
    int &mvy = mvar;
    char mybuff[] = "test!";
    thread myobj(myPrint, mvy, mybuff);
    myobj.join();
    std::cout << "Hello World!\n";
    system("pause");
    return 0;
}
虽然主线程和子线程的参数传递为值传递、但是不建议用detach、引用可能会出现问题、指针传递一定不安全

进入快速监视shift+F9

表达式 - 填入 
&mvar
可计算得
        名称        值            类型
+        &mvar    0x004ffc64 {1}    int *
&mvy
+        &mvy    0x004ffc64 {1}    int *
&mybuff
+        &mybuff    0x004ffc48 {116 't', 101 'e', 115 's', 116 't', 33 '!', 0 '\0'}    char[6] *

而多线程函数内
&i
+        &i    0x0064f3f4 {1}    const int *
&buff
+        &buff    0x00d2f298 {0x004ffc48 "test!"}    char * *

可以看到

主函数传入线程的参数mybuff是char*类型的、而子线程所对应参数的类型是char**类型的,保存的是传入参数的地址。是一种浅拷贝
拷贝构造、地址过早被释放

而引用值传递、实际上是拷贝构造函数在作用、可能会出现一种情况是、需要拷贝的东西比较多、而主线程结束较快、导致拷贝构造函数创造的对象所对应的地址过早被释放。拷贝内容不安全。

原因:这种构造是在子线程中进行的。

#include <iostream>
#include <thread>
using namespace std;
void myPrint(const string & i)
{
    cout << i << endl;
    return;
}

int main()
{
    string mystring("I love you!");
    thread myobj(myPrint, mystring);
    myobj.join();
    std::cout << "Hello World!\n";
    system("pause");
    return 0;
}

解决方法:手动构造一个对象、此时一定会先进行构造函数,然后再执行拷贝构造函数(时机不定)、能保证程序的稳定运行

此时临时参数的构造函数和拷贝构造函数都是在主线程中,不会造成处理异常。

...
thread myobj(myPrint, string(mystring));
...

2.线程id

每个线程对应一个数字,不同的线程对应线程ID不同

可通过std::this_thread::get_id()来获取;

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

a.主线程向子线程传递参数、子线程函数中的引用函数必须使用const、使用mutable

void myPrint(int & i, char *buff)
{
    cout << i << endl;
    cout << buff << endl;
    return;
}
严重性    代码    说明    项目    文件    行    禁止显示状态
错误    C2672    “std::invoke”: 未找到匹配的重载函数    线程传参    c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.16.27023\include\thr\xthread    238    

必须改为const int & i

void myPrint(const int & i, char *buff)
{
    cout << i << endl;
    cout << buff << endl;
    return;
}

可以通过声明mutable的方式来进行修改参数

class classA{
public:
    mutable int m_i; 
    classA(int i):m_i(i){};
}
void MyThreadProg(classA &myca)
{
    myca.m_i = 199;//可以不进行const、可以修改,但是由于是拷贝构造而来的、主线程的参数不会有所变化。
    cout << myca.m_i << "in child prog" << endl ;
}

此时,主函数中

...
    int i = 10;
    classA ca(i);
    thread mytd(MyThreadProg,ca;
    mytd.join();
    cout << ca.m_i << endl;
...

运行程序后、经过调试可以发现、子线程中修改的值,主线程没有变化、主线程的ca.m_i!=myca.m_i(子线程的)

此时,说明参数不是引用关系,前面也说过,是进行了拷贝构造函数。两者已经相互独立。

b.std::ref函数使参数转变成真引用

上例的主函数,通过std::ref函数,可改为:

...
    int i = 10;
    classA ca(i);
    thread mytd(MyThreadProg,std::ref(ca));
    mytd.join();
    cout << ca.m_i << endl;
...

类的声明不需要使用mutable,子线程函数也不用再加const

class classA{
public:
    int m_i;
    classA(int i):m_i(i){};
}
void MyThreadProg(classA &myca)
{
    myca.m_i = 199;
    cout << myca.m_i << "in child prog" << endl ;
}

此时、变为真引用,子线程的参数更改,主线程参数也会发生更改、并且不会对子函数变量再进行构造和拷贝构造。

c.智能指针作为参数如何进行传递

#include <iostream>
#include <thread>
using namespace std;
void myThreadProg(unique_ptr<int> pzn)
{
    cout << "子线程的参数地址为" << &pzn << endl;
}

int main()
{
    unique_ptr<int> mint(new int(100));
    cout << &mint << endl;
    std::thread mytd(myThreadProg, std::move(mint));
    system("pause");
    return 0;
}

地址:

+        [ptr]    0x0139d858 {100}    int *
+        [ptr]    0x0139d858 {100}    int *

但是要注意:

(含糊:)主线程执行完后,堆区空间被释放、子线程还未执行完的话,会出现内存泄漏。

d.类函数

&classA::classProg

四、创建多个线程、数据共享问题分析、案例代码

1.创建和等待多个线程

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

void MyThreadProg(int i)
{
    cout << "MyThreadProg is begin " << endl << i << endl;
    return;
}
int main()
{
    vector <thread> mythreads;
    //创建10个线程、线程入口函数统一使用MyThreadProg
    for (int i = 0; i<10; i++)
    {
    
        mythreads.push_back(thread(MyThreadProg,10));//创建10个线程,同时这个10个线程已经开始执行
    }
    for (auto iter = mythreads.begin() ; iter != mythreads.end(); iter++)
    {
        iter->join();//等待10个线程交汇
    }
    system("pause");
    std::cout << "Hello World!\n";
}

多个线程执行顺序是乱的,跟操作系统内部对线程的运行调度机制有关。推荐使用join写法,主线程等待所有子线程结束。

2.数据共享问题分析

a.只读不写、线程安全、可同时读

vector <int> gInt = {1,2,3};

b.有读有写、线程不安全

3.问题引出-共享数据需要保护

引出-共享数据不保护,边读边写会报错。

vector,list:list对频繁按顺序插入和删除数据时效率更高,vector容器随机的插入和删除效率更高。

list:
底层结构是带头节点的双向循环链表
在任意位置插入和删除的效率高,时间复杂度为O(1)
不支持随机访问
迭代器失效:插入元素不会导致迭代器失效,删除时只会导致当前迭代器失效,其他迭代器不受影响,重新赋值当前迭代器即可

vector:
底层结构是动态顺序表
在任意位置插入和删除效率低,插入有可能会增容,会导致开辟新空间,拷贝元素释放旧空间使效率更低
支持随机访问
迭代器失效:在插入元素可能会扩容导致原有迭代器失效所有迭代器需要重新赋值,删除时当前迭代器会失效需要重新赋值

使用场景
有大量插入和删除操作,不关心随机访问使用list
需要高效存储,支持随机访问,不关心插入删除效率时使用vector
#include <iostream>
#include <thread>
#include <map>
#include <vector>
#include <list>
using namespace std;
class classA {
private:
    std::list<int> msgRecvQueue;//容器,专门用于代表玩家发送过来的命令
public:
    void inMsgRecvQueue()
    {
        for (int i = 0; i<10000;i++)
        {
            cout << "inMsgRecvQueue执行,插入一个元素" << i << endl;
            msgRecvQueue.push_back(i);//假设这个数字i就是我收到的命令,我直接放入消息队列里
        }
    
    }
    //把数据从消息队列取出的线程
    void outMsgRecvQueue()
    {
        for (int i = 0; i < 10000; i++)
        {
            if (!msgRecvQueue.empty())
            {
                //消息队列不为空
                int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
                msgRecvQueue.pop_front();//移除第一个元素,但不返回。
                
            }
            else
            {
                cout << "outMsgRecvQueue()执行,但消息队列中为空" << i << endl;
            }    
        }
        cout << "end" << endl;
    }
};
int main()
{
    classA ca;
    std::thread myOutnMsgObj(&classA::outMsgRecvQueue, &ca);//第二个参数用 引用
    std::thread myInnMsgObj(&classA::inMsgRecvQueue, &ca);
    myOutnMsgObj.join();
    myInnMsgObj.join();
}

五、互斥量概念、用法、死锁演示及解决方案

1.mutex

#include <mutex>
std::mutex m_mutex
...
m_mutex.lock();
//需要保护的资源
m_mutex.unlock();
...

2.std::lock_guard类模板

直接取代lock和unlock

#include <mutex>
std::mutex m_mutex
...
{
    std::lock_guard<std::mutex> lg(m_mutex);//使用lock_gurad类模板创建一个自动资源锁(构造函数内有lock,析构函数里有unlock)、会保护同一作用域的变量(同一{}内的)
    //需要保护的资源
}
...

3.死锁

死锁必须两把不同的锁、互相需要对方的锁才能进行下去

...
thread_Prog1
{
    mutex1.lock();
    mutex2.lock();
    ...
}
thread_prog2
{
    mutex2.lock();
    mutex1.lock();
    ...
}
...

死锁的解决方法、

a.锁的顺序一致

...
thread_Prog1
{
    mutex1.lock();
    mutex2.lock();
    ...
}
thread_prog2
{
    mutex1.lock();
    mutex2.lock();
    ...
}
...

b.std::lock()函数模板、锁两个锁

std::lock(mutex1,mutex2);//如果锁另一个没锁住、则会放开已经锁住的那个

c.std::lock_guard()的std::adopt_lock参数

...
std::lock(mutex1,mutex2);
//需要保护的资源
std::lock_guard<std::mutex> lg1(mutex1,std::adopt_lock);//适应现在的lock状态、不会再调用lock
std::lock_guard<std::mutex> lg2(mutex2,std::adopt_lock);//适应现在的lock状态、不会再调用lock
...

六、unique_lock类模板

unique_lock是个类模板,工作中一般推荐使用lock_guard,lock_guard取代了mutex的lock()和unlock()

unique_lock比lock_guard灵活很多;效率上差一点,内存占用多一点。

#include <mutex>
std::mutex m_mutex
{
    std::unique_lock<std::mutex> lg(m_mutex);
    //需要保护的资源
}

1.std::adopt_lock

...
    std::unique_lock<std::mutex> lg(m_mutex,std::adopt_lock);//不会再调用lock
...

2.std::try_to_lock,没有锁定成功,也会立即返回,并不会阻塞

...
{
    std::unique_lock<std::mutex> lg(m_mutex,std::try_to_lock);//尝试拿锁、不能自己lock、如果能拿到锁、就会帮你锁了
    if(lg.owns_lock())//拿到锁
    {
        //受保护的资源
    }
    else{
        //没拿到锁、干点别的事儿啊
    }
}
...

3.std::defer_lock、初始一个没有加锁的mutex

...
{
    std::unique_lock<std::mutex> lg(m_mutex,std::defer_lock);//创建一个未锁的metux
    lg.lock();//加锁、并无需自己解锁。
    //受保护的资源
    //lg.unlock();//无需解锁,自动解锁、但是在同一作用域内,可以手动再解锁
}
...

4.unique_lock的try_lock()函数

...
std::unique_lock<std::mutex> lg(m_mutex,std::defer_lock);//创建一个未锁的metux
if (lg.try_lock() == true)
{
//受保护的资源
}
else{//没拿到锁
//做点别的事
}
...

5.unique_lock的release()函数,返回他所管理的mutex对象指针,并释放所有权(unique_lock与当前mutex不再有关系)、如果原本的unique_lock处于加锁的状态,则后续需要自己解锁

std::unique_lock<std::mutex> lg(m_mutex,std::defer_lock);//创建一个未锁的metux
std::mutex *pmt = lg.release();//则后期需要自己加锁、解锁

6.unique_lock所有权的传递

std::unique_lock<std::mutex> lg1(m_mutex1);
std::unique_lock<std::mutex> lg2(std::move(lg1));//相当于把m_mutex1绑定到

七、单例设计模式共享数据分析、解决、call_once

1.设计模式大概谈

a.单例设计模式

整个项目中,某个或者某些特殊的类,属于该类的对象,只能创建一个。

class classA
{
public:
    
    classA(const classA &) = delete;        //禁止生成默认拷贝构造函数
    classA(classA &&) = delete;                //禁止生成默认移动构造函数
    classA &operator=(const classA &) = delete;//禁止生成默认赋值函数
    classA &operator=(classA &) = delete;    //禁止生成默认赋值函数
private:
    classA();                        //处于私有变量状态,不能使用构造函数
    static classA *m_Object;//静态成员变量
public:
    static classA *Getm_Object()
    {
        if (m_Object == NULL)//保证只能创建一个对象
        {
            m_Object = new classA();
            static DeleteMyclass dmc;//创建一个静态对象、程序退出、一定会析构本类、释放本类的资源
        }
        return m_Object;//如果没有创建成功新的对象(说明当前由该类已经生成了一个对象),则返回当前对象的地址。
    }
    class DeleteMyclass//专门用来释放父类的资源
    {
    public:
        ~DeleteMyclass()
        {
            if (classA::m_Object == NULL)//如果不为空
            {
                delete classA::m_Object;//释放
                classA::m_Object == NULL;//防止野指针
            }
        }
    };
};
//类静态变量初始化
classA *classA::m_Object = NULL;//静态成员值对于该类声明的所有对象是唯一的
classA *ca = classA::Getm_Object();//返回该类的指针
//classA ca;//无法创建对象

推荐在主线程中、创建其他线程之前创建对象,经初始化、装载数据后然后在子线程中使用。

b.单例设计模式共享数据分析-两个同入口函数线程之间资源争夺

创建两个相同入口函数的线程、则两线程对全局变量存在资源争夺

... 
void tdProg()//线程函数
{
    classA *ca = classA::Getm_Object();//返回该类的指针
    return;
}
int main()
{
    //创建两个相同入口函数的线程、则两线程对全局变量存在资源争夺
    std::thread mtd1(tdProg);
    std::thread mtd2(tdProg);
    ...
}

加锁

std::mutex mtx1;
void tdProg()//线程函数
{
    std::unique_lock<std::mutex> uql(mtx1);
    classA *ca = classA::Getm_Object();//返回该类的指针
    return;
}
如果仅仅需要锁一次,(例如初始化)、则使用双检查锁、这样更高效
std::mutex mtx1;
void tdProg()//线程函数
{
    if (m_value == 0)
    {
        std::unique_lock<std::mutex> uql(mtx1);
        if (m_value == 0)
        {
            //不能将锁放到最里层,因为、阻塞状态一段时间后、等另一个线程释放锁后,还会继续执行
            m_value = 10;//初始化or赋值
        }
     }
}

2.call_once函数模板、保证线程函数只会被调用一次

std::once_flag g_flag;//定义一个标记

void tdOnceProg()
{
    //只需要执行一次的程序
}
void tdProg()
{
    ...
    //tdOnceProg();//调用了一次该程序
    std::call_once(g_flag,tdOnceProg);
    ...
}
int main()
{
    //创建两个线程、线程函数相同
    std::thread mtd1(tdProg);
    std::thread mtd2(tdProg);
}

3.线程等待函数

void tdProg()//线程函数
{
    std::chrono::milliseconds delay_20s(20000);
    std::this_thread::sleep_for(delay_20s);
}

八、condition_variable、wait、notify_one、notify_all

1.条件变量

wait函数和unique_lock目标类对象结合,可以实现一个线程等待另一个线程完成数据的采集后处理数据。

#include <iostream>
#include <mutex>
#include <list>
#include <condition_variable>

class classA
{
public:
    void Msgget();
    void MsgDeal();
private:
    std::condition_variable m_cdv;    //创建一个条件变量对象
    std::mutex m_mtx;                        //生成一个互斥量
    std::list<int> m_msgRecvQueue;        //声明一个消息队列的容器
};

void classA::Msgget()//消息的生产者
{
    for (int i=0;i<10000;i++)
    {
        std::unique_lock<std::mutex> uql(m_mtx);
        m_msgRecvQueue.push_back(i);
        m_cdv.notify_one();//唤醒其他线程因wait阻塞的程序,告诉他自己生产完数据了。
    }
}

void classA::MsgDeal()//消息的消费者
{
    int commad = 0;
    while (true)
    {
        std::unique_lock<std::mutex> uql(m_mtx);//自动解锁
        ///waite用来等一个东西
        //如果第二个参数lambda表达式返回的是false,那么wait将解锁互斥量,并堵塞到本行。如果第二个条件是TRUE、
            //堵塞到其他某个成员函数调用notify_one()成员函数为止;当wait()被唤醒时,尝试锁上互斥量,锁不上就阻塞。锁上互斥量后对lambda表达式进行判断、lambda表达式为false则又会解锁互斥量。是true的话继续往下执行
        //如果wait没有第二个参数,默认参数为false、当wait()被唤醒时,锁上互斥量。进行下一步的程序
        m_cdv.wait(uql,
            [this] {if (!m_msgRecvQueue.empty())//如果队列不为空
                return true;
            else
                return false;}
        );
        //消息队列里可能不止一条数据、应该吧所有的数据都处理了
        commad = m_msgRecvQueue.front();//返回第一像素,但不检查元素是否存在;
        m_msgRecvQueue.pop_back();//移除第一个元素,但不返回;
        uql.unlock();//数据取出来了,就可以手动解锁
        std::cout << commad << "by MsgDeal" << std::endl;
    }
}

2.notify_all-一生产者多消费者模式、唤醒多个消费者线程

m_cdv.notify_all();

3.虚假唤醒

std::condition_variable::notify_one()std::condition_variable::notify_all()

std::condition_variable::wait()

  • 原因:调用多次notify_one()、或者notify_all()
  • wait()被唤醒后,实际线程对应资源不存在
  • 解决方法:使用lambda表达式,来屏蔽虚假唤醒、先拿锁、然后判断对应资源是否存在

    m_cdv.wait(uql,[this] {if (!m_msgRecvQueue.empty())//如果队列不为空,详见上文-条件变量示例

4.数据堆积

唤醒后,没拿到锁,数据来不及处理造成数据堆积

解决:拿到锁后判断待处理的数据量,然后进行处理。

九、async、future、packaged_task、promise

1.async、future创建后台任务并返回值

std::async是一个函数模板、用来启动一个异步任务,启动起来一个异步任务后,返回一个std::future对象,

启动一个异步任务-自动创建一个线程并开始执行对应的线程入口函数,它返回一个std::future对象,

std::future可以用来获取异步任务的结果,因此可以把它当成一种简单的线程间同步的手段。std::future 通常由某个 Provider 创建,你可以把 Provider 想象成一个异步任务的提供者,Provider 在某个线程中设置共享状态的值,与该共享状态相关联的 std::future 对象调用 get(通常在另外一个线程中) 获取该值,如果共享状态的标志不为 ready,则调用 std::future::get 会阻塞当前的调用者,直到 Provider 设置了共享状态的值(此时共享状态的标志变为 ready),std::future::get 返回异步任务的值或异常(如果发生了异常)。

一个有效(valid)的 std::future 对象通常由以下三种 Provider 创建,并和某个共享状态相关联。Provider 可以是函数或者类,其实我们前面都已经提到了,他们分别是:

如果子线程没有处理完,std::future::get()函数会使主线程阻塞、直到得到结果、注意get()只能调用一次、因为get()类似于移动语义,移走了就没了

#include <iostream>
#include <list>
#include <mutex>
#include <future>

int tdProg()
{
    std::cout << "tdProg id = " << std::this_thread::get_id() << std::endl;//打断线程id
    std::chrono::microseconds cm(5000);//休息5s
    std::this_thread::sleep_for(cm);
    return 5;
}

int main()
{
    std::cout << "mian" << std::this_thread::get_id() << std::endl;
    std::future<int> result = std::async(tdProg);//创建一个将来能接收的值,自动创建线程并开始执行线程入口函数
    int def = 0;
    std::cout << result.get() << std::endl;//如果子线程没有处理完,get()函数会使主线程阻塞、直到得到结果
}

主线程会一直等待async创建的线程执行完毕,才回退出主线程????????????存疑?????未确定????

#include <iostream>
#include <future>
using namespace std;
int myProg()
{
    std::chrono::seconds cs(5);
    std::this_thread::sleep_for(cs);//休息5s
    cout << "线程执行完毕" << endl;
    return -1;
}
int main()
{
    std::future<int> result = std::async(myProg);
    std::future_status statue = result.wait_for(std::chrono::seconds(1));//等待1s
    if (statue == std::future_status::timeout)//超时、表明线程还没执行完
    {
        cout << "超时了" << endl;
    }
    std::cout << "Hello World!\n";
    system("pause");//在此处等待、或者掐断点。会发现最后会输出“线程执行完毕”字样
    return 0;
}

std::future::wait()、只会等待线程结束、不会获取线程的结果

将std::async()用于类成员函数

#include <iostream>
#include <list>
#include <mutex>
#include <future>
class classA
{
public:
    int tdProg(int a)
    {
        std::cout << "tdProg id = " << std::this_thread::get_id() << std::endl;//打断线程id
        std::cout << a << std::endl;
        std::chrono::microseconds cm(5000);//休息5s
        std::this_thread::sleep_for(cm);
        return 5;
    }
};


int main()
{
    classA ca;
    std::cout << "mian" << std::this_thread::get_id() << std::endl;
    std::future<int> result = std::async(&classA::tdProg, &ca,5);//创建一个将来能接收的值,自动创建线程并开始执行线程入口函数
    int def = 0;
    std::cout << result.get() << std::endl;//如果子线程没有处理完,get()函数会使主线程阻塞、直到得到结果
}
class classA
{
public:
    int tdProg(int a)
    {
        std::cout << "tdProg id = " << std::this_thread::get_id() << std::endl;//打断线程id
        std::cout << a << std::endl;
        std::chrono::microseconds cm(5000);//休息5s
        std::this_thread::sleep_for(cm);
        return 5;
    }
};
classA ca;
std::future<int> result = std::async(&classA::tdProg, &ca,5);//第二个参数是对象引用,

2.std::launch的额外参数

通过额外向std::launch类型,来达到一些特殊的目的;

a.std::launch::deferred-延迟调用,不创建新线程

表示线程入口函数调用被延迟到std::future的wait()或get()函数调用时才执行-实际是在主线程中执行了、没有新建线程

如果wait()和get()没有被调用,线程会执行吗?不会。

用法

...
std::future<int> result = std::async(std::launch::deferred,&classA::tdProg, &ca,5);
...

b.std::launch::async-强制异步任务必须创建新线程,并立即执行

用法

...
std::future<int> result = std::async(std::launch::async,&classA::tdProg, &ca,5);
...

c.默认参数 std::launch::async | std::launch::deferred、系统自动判断

用法

...
std::future<int> result = std::async(std::launch::async | std::launch::deferred,&classA::tdProg, &ca,5);
...

等同于

...
std::future<int> result = std::async(&classA::tdProg, &ca,5);
...

3.std::async与std::thread的区别

a.std::thread,强制创建线程,如果系统资源紧张,线程创建失败,程序崩溃

b.std::async创建异步任务,是尝试创建,无法创建新线程,则在std::future::get里得到值

c.std::async | std::deferred - 系统自行选择时

d.判断std::async是否创建新线程

#include <iostream>
#include <future>
using namespace std;
int myProg()
{
    std::chrono::seconds cs(5);
    std::this_thread::sleep_for(cs);//休息5s
    cout << "线程执行完毕" << endl;
    return -1;
}
int main()
{
    std::future<int> result = std::async(myProg);
    //std::future_status statue = result.wait_for(0s); //0min、是一种 s和min运算符重载
    std::future_status statue = result.wait_for(std::chrono::seconds(0));//等待0s
    if (statue == std::future_status::timeout)//超时、表明线程还没执行完
    {
        cout << "超时了" << endl;
    }
    else if (statue == future_status::ready)//线程执行完毕
    {
        cout << "线程成功返回" << endl;
    }
    else if (statue == future_status::deferred)//延迟、没有创建子线程、直接在主线程执行的
    {
        //调用get();确保线程函数执行
        cout << result.get() << endl;
    }
    std::cout << "Hello World!\n";
    system("pause");
    return 0;
}

4.std::packaged_task:打包任务,把任务包装起来

#include <iostream>
#include <list>
#include <mutex>
#include <future>
class classA
{
public:
    int tdProg(int a)
    {
        std::cout << "tdProg id = " << std::this_thread::get_id() << std::endl;//打断线程id
        std::cout << a << std::endl;
        std::chrono::microseconds cm(5000);//休息5s
        std::this_thread::sleep_for(cm);
        return 5;
    }
};


int main()
{
    classA ca;
    std::cout << "mian" << std::this_thread::get_id() << std::endl;
    //std::future<int> result = std::async(&classA::tdProg, &ca,5);//创建一个将来能接收的值,自动创建线程并开始执行线程入口函数
    //packaged_task类模板也是定义于future头文件中,它包装任何可调用 (Callable) 目标,包括函数、 lambda 表达式、 bind 表达式或其他函数对象,使得能异步调用它,
    //其返回值或所抛异常被存储于能通过 std::future 对象访问的共享状态中。简言之,将一个普通的可调用函数对象转换为异步执行的任务。
    std::packaged_task<int(int)> mpt(&classA::tdProg);//函数模板<int(int)> 返回值是int、参数也是int
    std::thread mtd(std::ref(mpt), &ca,1);//线程直接开始执行
    mtd.join();
    std::future<int> result = mpt.get_future();//std::future对象里面包含线程入口函数的返回结果,这里的reusult保存tdProg的返回结果
    std::cout << result.get() << std::endl;//如果子线程没有处理完,get()函数会使主线程阻塞、直到得到结果
}

5.std::promise,类模板

在某个线程中给他赋值,然后可以在其他线程中,把这个值取出来用;

#include <iostream>
#include <list>
#include <mutex>
#include <future>

void tdProg1(std::promise<int> &temp, int calc)
{
    //做一系列复杂的操作
    std::cout << calc << std::endl;
    std::chrono::microseconds cm(5000);//休息5s
    std::this_thread::sleep_for(cm);
    int result = calc;//得到的结果
    temp.set_value(result);//保存到结果值里面
}

int main()
{
    std::promise<int> mps;//声明一个std::promis对象,保存的值类型为int;
    std::thread mtd(tdProg1,std::ref(mps),80);
    mtd.join();//可以没有、可以用detach
    //获取结果值
    std::future<int> mft = mps.get_future();//用于获取线程返回值
    auto result = mft.get();
    std::cout << result << std::endl;
    return 0;
}

//线程间互传数据、一个线程等待另一个线程的数据

#include <iostream>
#include <list>
#include <mutex>
#include <future>

void tdProg1(std::promise<int> &temp, int calc)
{
    //做一系列复杂的操作
    std::cout << calc << std::endl;
    std::chrono::milliseconds cm(5000);//休息5s
    std::this_thread::sleep_for(cm);
    int result = calc;//得到的结果
    temp.set_value(result);//报错到结果值里面
    return;
}

void tdProg2(std::future<int> &temp)
{
    //做一系列复杂的操作
    std::cout << temp.get() << std::endl;
    return;
}

int main()
{
    std::promise<int> mps;//声明一个std::promis对象,保存的值类型为int;
    std::thread mtd1(tdProg1,std::ref(mps),80);
    //获取结果值
    mtd1.detach();
    std::future<int> mft = mps.get_future();//用于获取线程返回值

    std::thread mtd2(tdProg2, std::ref(mft));
    //auto result = mft.get();
    //std::cout << result << std::endl;
    mtd2.join();
    system("pause");
    return 0;
}

十、future其他成员函数、shared_future、automic

1.std::future的其他成员函数

#include <iostream>
#include <future>
using namespace std;
int myProg()
{
    std::chrono::seconds cs(5);
    std::this_thread::sleep_for(cs);//休息5s
    cout << "线程执行完毕" << endl;
    return -1;
}
int main()
{
    std::future<int> result = std::async(myProg);
    std::future_status statue = result.wait_for(std::chrono::seconds(1));//等待1s
    if (statue == std::future_status::timeout)//超时、表明线程还没执行完
    {
        cout << "超时了" << endl;
    }
    else if (statue == future_status::ready)//线程执行完毕
    {
        cout << "线程成功返回" << endl;
    }
    else if (statue == future_status::deferred)//延迟、而且没有创建子线程、直接在主线程执行的
    {
        //如果async的第一个参数设置为std::launch::deferred,则本条件成立
        cout << result.get() << endl;
    }
    std::cout << "Hello World!\n";
    system("pause");
    return 0;
}

2.std::shared_future,get()-复制语义、可以多次get()

std::shared_futurestd::future 类似,但是 std::shared_future 可以拷贝、多个 std::shared_future 可以共享某个共享状态的最终结果(即共享状态的某个值或者异常)。shared_future 可以通过某个 std::future 对象隐式转换(参见 std::shared_future 的构造函数),或者通过 std::future::share() 显示转换,无论哪种转换,被转换的那个 std::future 对象都会变为 not-valid.

std::shared_future 构造函数

std::shared_future 共有四种构造函数,如下表所示:

构造函数类型构造函数
默认构造函数shared_future() noexcept;
拷贝构造函数shared_future (const shared_future& x);
移动构造函数shared_future (shared_future&& x) noexcept;
自future而来的移动构造shared_future (future<T>&& x) noexcept;
#include <iostream>
#include <list>
#include <mutex>
#include <future>
#include <iostream>
#include <list>
#include <mutex>
#include <future>

void tdProg1(std::promise<int> &temp, int calc)
{
    //做一系列复杂的操作
    std::cout << calc << std::endl;
    std::chrono::milliseconds cm(5000);//休息5s
    std::this_thread::sleep_for(cm);
    int result = calc;//得到的结果
    temp.set_value(result);//报错到结果值里面
    return;
}

void tdProg2(std::shared_future<int> &temp)
{
    //做一系列复杂的操作
    std::cout << temp.get() << std::endl;
    return;
}

int main()
{
    std::promise<int> mps;//声明一个std::promis对象,保存的值类型为int;
    std::thread mtd1(tdProg1, std::ref(mps), 80);
    //获取结果值
    mtd1.detach();
    //std::future<int> mft = mps.get_future();//用于获取线程返回值
    //std::shared_future<int> msf(mft.share());//future移动构造
    //std::shared_future<int> msf(std::move(mft));//移动构造
    std::shared_future<int> msf = mps.get_future();//用shared_future类模板创建
    std::thread mtd2(tdProg2, std::ref(msf));
    auto result = mtd2.get();//可以多次get()
    std::cout << result << std::endl;
    mtd2.join();
    system("pause");
    return 0;
}

3.原子操作:std::atomic用法范例

两个线程对同一个资源进行处理时会发生资源抢占的现象、所以必须有互斥量解决。使用原子操作可以增加效率。(无锁)的多线程并发编程方式,是一种在多线程中不会被打断的程序执行片段。

限制:原子操作只是对一个变量声明临界,而互斥量是针对一段代码段

std::atomic是个类模板,是用来封装某个类型的值

#include <iostream>
#include <atomic>
#include <thread>
std::atomic<long> gmint = 0;
void mtdProg()
{
    for (long i =0; i<2000000; i++)
    {
        gmint++;
    }
}
int main()
{
    //创建两个同函数的线程
    std::thread mytd1(mtdProg);
    std::thread mytd2(mtdProg);
    mytd1.join();
    mytd2.join();
    std::cout << gmint << std::endl;
    system("pause");
}

4.原子操作使用范围

i++;
++i;
i+=1;
i&=1;
i|=1;
i^=1;

其他方法起不到保护作用。

5.std::atomic::load()-以原子方式读参数值、std::atomic::store()-以原子方式写值

std::atomic<int> matm;
std::atomic<int> matm1(matm.load());
×//std::atomic<int> matm1(matm);//没有拷贝构造函数
auto matm3(matm.load());
matm3.store(12);

十一、windows临界区,其他各种mutex互斥量

1.windows临界区

使用CRITICAL_SECTION声明

InitializeCriticalSection();初始化

EnterCriticalSection();进入临界区

LeaveCriticalSection();出临界区

示例

#include <iostream>
#include <thread>
#include <map>
#include <vector>
#include <list>
#include <mutex>
#include <windows.h>
#define __WINDOWSJQ_
using namespace std;
class classA {
private:
    std::list<int> msgRecvQueue;//容器,专门用于代表玩家发送过来的命令
#ifdef __WINDOWSJQ_
    CRITICAL_SECTION my_winsec;//windows中的临界区,非常类似于C++11中的mutex
#endif // __WINDOWSJQ_

    std::mutex m_mutex;
public:
    classA()
    {
#ifdef __WINDOWSJQ_ 
        InitializeCriticalSection(&my_winsec);//windows临界区必须初始化
#endif // __WINDOWSJQ_ 

    }
    void inMsgRecvQueue()
    {
        for (int i = 0; i < 10000; i++)
        {
#ifdef __WINDOWSJQ_ 
            EnterCriticalSection(&my_winsec);//进入临界区
            cout << "inMsgRecvQueue执行,插入一个元素" << i << endl;
            msgRecvQueue.push_back(i);//假设这个数字i就是我收到的命令,我直接放入消息队列里
            LeaveCriticalSection(&my_winsec);//出临界区
#else
            m_mutex.lock();
            cout << "inMsgRecvQueue执行,插入一个元素" << i << endl;
            msgRecvQueue.push_back(i);//假设这个数字i就是我收到的命令,我直接放入消息队列里
            m_mutex.unlock();
#endif // __WINDOWSJQ_ 
        }

    }
    //把数据从消息队列取出的线程
    void outMsgRecvQueue()
    {
        for (int i = 0; i < 10000; i++)
        {
            if (!msgRecvQueue.empty())
            {
#ifdef __WINDOWSJQ_ 
                EnterCriticalSection(&my_winsec);//进入临界区
                if (!msgRecvQueue.empty())
                {
                    //消息队列不为空
                    int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
                    msgRecvQueue.pop_front();//移除第一个元素,但不返回。

                }
                LeaveCriticalSection(&my_winsec);//出临界区
#else
                m_mutex.lock();
                if (!msgRecvQueue.empty())
                {
                    //消息队列不为空
                    int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
                    msgRecvQueue.pop_front();//移除第一个元素,但不返回。

                }
                m_mutex.unlock();
#endif // __WINDOWSJQ_ 
            }
            else
            {
                cout << "outMsgRecvQueue()执行,但消息队列中为空" << i << endl;
            }

        }
        cout << "end" << endl;
    }
};
int main()
{
    classA ca;
    std::thread myOutnMsgObj(&classA::outMsgRecvQueue, &ca);//第二个参数用 引用
    std::thread myInnMsgObj(&classA::inMsgRecvQueue, &ca);
    myOutnMsgObj.join();
    myInnMsgObj.join();
}

2.多次进入临界区实验

  • windows临界区,同一个线程中允许重复进入,必须进入临界区的次数必须与出的临界区次数相同、而mutex不允许lock多次

3.自动析构技术

...
std::mutex m_mutex;
{
    std::lock_guard<std::mutex> mlg(m_mutex);
    //业务代码、受保护的资源
};
...

4.std::recurisve_mutex递归的独占互斥量

  • std::mutex 独占互斥量
  • std::recurisve_mutex 递归的独占互斥量、同一个线程,同一个互斥量,能够多次lock()
  • std::recurisve_mutex 可lock,可被unlock
  • 递归次数有限制
std::recurisve_mutex mmutex;
std::lock_guard<std::recurisve_mutex> mlg(m_mutex);
std::lock_guard<std::recurisve_mutex> mlg(m_mutex);

5.带超时的互斥量std::timed_mutex和std::recursive_timed_mutex

  • std::timed_mutex:带超时功能的独占互斥量

    • try_lock_for():等待一段时间,如果拿到锁,或者等待超时没拿到锁,就走下来
    std::timed_mutex mtm;
    std::chrono::milliseconds timeout(100);
    if (mtm.try_lock_for(timeout))//等待100ms尝试拿锁
    {
        //拿到了锁、工作开始
        mtm.unlock();
    }
    else
    {
        //没拿到锁、其他操作
    }
    • try_lock_until():是一个未来的时间点,如果拿到锁,或者等待超时没拿到锁,就走下来
    std::timed_mutex mtm;
    std::chrono::milliseconds timeout(100);
    if (mtm.try_lock_until(std::chrono::steady_clock::now() + timeout))//等待100ms尝试拿锁
    {
        //拿到了锁、工作开始
        mtm.unlock();
    }
    else
    {
        //没拿到锁、其他操作
    }
  • std::recursive_timed_mutex:带超时功能的递归互斥量
Last modification:May 29th, 2022 at 11:49 am