image frame

星际旅行日志

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

c++11并发与多线程【第十一节】std::atomic续谈、std::async深入谈

第十一节 std::atomic续谈、std::async深入谈

一、原子操作std::atomic续谈?

上一篇文章里,我们使用原子操作使得线程内的g_mycout自增++不会被其他线程打断,进而获取正确的结果:

#include<iostream>
#include<vector>
#include<thread>
#include<string>
#include<list>
#include<mutex>
#include <future>
using namespace std;
std::atomic<int> g_mycout(0);//封装了一个类型为int的对象,这是一个可以执行原子操作的整型变量
void mythread(){
    for(int i=0;i<10000000;i++){
        g_mycout++;
    }
    return;
}
int main(){
    thread mytobj1(mythread);
    thread mytobj2(mythread);
    mytobj1.join();
    mytobj2.join();
    cout<<"程序执行完毕, result = "<<g_mycout<<endl;
    return 0;
}

程序执行完毕, result = 20000000

如果将g_mycout++改为g_mycout=gmycout+1结果是否相同?

void mythread(){
    for(int i=0;i<10000000;i++){
        g_mycout=g_mycout+ 1;
        //g_mycout++;
    }
    return;
}

程序执行完毕, result = 19897970

此时结果是错误的。一般的原子操作,针对++,–,+=,&=,|=,*=,-=是支持的,其他的可能不支持。如果使用时不确定是否支持,则需要自己像上面一样进行测试。

二、std::async深入谈

2.1 std::async参数详述,async用来创建一个异步任务

int mythread(){
    cout<<"thread ID = "<<std::this_thread::get_id()<<"结束"<<endl;
    return 1;
}
int main(){
    cout<<"MAIN thread ID = "<<std::this_thread::get_id()<<endl;
    std::future<int> result=std::async(mythread);
    cout<<result.get()<<endl;
    return 0;
}

之前使用过的参数,std::launch::deferred【延迟调用】,以及std::launch::async【强制创建一个线程】;
std::thread()如果系统资源紧张,可能创建线程会失败,那么执行std::thread()时整个线程会崩溃。
std::async()我们一般不叫创建线程,一般叫做创建一个异步任务;
std::asyncstd::thread明显的不同就是async有时候并不创建线程;

  1. 使用std::launch::deferred来调用async
std::future<int> result=std::async(std::launch::deferred,mythread);
int main(){
    cout<<"MAIN thread ID = "<<std::this_thread::get_id()<<endl;
    std::future<int> result=std::async(std::launch::deferred,mythread);
    cout<<result.get()<<endl;
    return 0;
}

MAIN thread ID = 140737348093760
thread ID = 140737348093760结束
1

此时没有创建新线程。deferred延迟调用,并且不创建新线程,延迟到future对象调用get()或者wait()时才执行mythread();
2. 如果使用的是std::launch::async,强调这个异步任务在新线程上运行,则必须要创建出一个新线程。

std::future<int> result=std::async(std::launch::async,mythread);

MAIN thread ID = 140737348093760
thread ID = 140737348089600结束
1

  1. 同时使用std::launch::deferred|std::launch::async
std::future<int> result=std::async(std::launch::async|std::launch::deferred,mythread);

MAIN thread ID = 140737348093760
thread ID = 140737348089600结束
1

看上去和std::launch::async差不多,其实不一样。这个 | 意味着调用async的行为可能是创建新线程并立即执行,也可能是不创建新线程并延迟调到调用future类的get()才执行。具有不确定性。

  1. 不带参数,只给std::async()一个入口函数名
std::future<int> result=std::async(mythread);

第九节中,有错误,当不带参数时,他说默认值是std::launch::async 是错误的,默认值应该是std::launch::deferred|std::launch::async ,换句话说系统会自行决定是异步(创建新线程)还是同步运行(不创建新线程)。那么如何自行决定?看下面

2.2 std::async与std::thread的区别

std::thread创建线程,如果系统资源紧张,创建线程失败。整个程序就会崩溃。std::thread创建线程的方式,如果想接受线程返回值,只能用全局变量。
std::async创建异步任务,可以创建新线程也可以不创建。并且async调用方法很容易拿到线程入口函数的返回值。
由于系统资源限制:
如果用std::thread创建的线程太多,再用std::thread可能会失败,系统报告异常崩溃。
如果用std::async,一般就不会报异常崩溃,因为系统资源紧张时无法创建新线程时std::async这种不加额外参数的调用,就不会创建新线程,而是后续谁调用future类的get()或者wait(),谁就执行那个入口函数,即异步任务就运行在执行get()的语句的线程上。

如果强制std::async一定要创建新线程,那么就必须使用std::launch::async参数,代价就是系统资源紧张时可能崩溃。

经验:一个程序里,线程数量不宜超过100-200。

2.3 std::async的不确定性的解决

不加额外参数的std::async调用,让系统自行决定是否创建新线程,问题在于std::launch::deferred | std::launch::async 这种写法,这个异步任务到底有没有被推迟执行,还是创建了新线程。可以使用条件判断的方法来确定。

int mythread(){
	//执行一些操作
    cout<<"thread ID = "<<std::this_thread::get_id()<<"结束"<<endl;
    return 1;
}
int main(){
    cout<<"MAIN thread ID = "<<std::this_thread::get_id()<<endl;
    std::future<int> result=std::async(std::launch::async|std::launch::deferred,mythread);
    std::future_status status= result.wait_for(std::chrono::seconds(0));
    if(status==std::future_status::deferred)
    {
        //线程被推迟执行了,系统资源紧张了,采用延迟调用
        cout<<result.get()<<endl;
    }
    else if(status==std::future_status::ready){
        //任务没有被推迟执行,创建新线程
        //且线程执行成功并返回
        cout<<result.get()<<endl;//获取线程返回值
    }
    else if(status==std::future_status::timeout){
        //超时线程还没执行完(如果在线程入口函数里执行的时间较长)
        cout<<"超时线程还没执行完"<<endl;
        cout<<result.get()<<endl;
    }    
    return 0;
}

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

Ubuntu20.04下VTK9.0.1的安装与在Qt5.12下的案例测试和使用CMAKE的测试

前言

本文记录了在Ubuntu20.04下VTK9.0.1的安装,两种使用方式:在Qt5.12下的案例测试,单独使用CMAKE的来运行案例程序的测试。

一、Ubuntu20.04下VTK9.0.1的安装

实际上在ubuntu20.04环境下安装VTK9.0.1与VTK8.2.0差不多,CSDN上的资料很多了,我是按照这篇文章来安装的,一路畅通,不通的话再百度,ubuntu18.04安装vtk8.2.0,所以就直接指路了。但是他的最后一步,我弄了之后出了一些问题,反正也用不到,最好不要弄。

二、VTK9.0.1在QT5.12下的使用

2.1 新建一个工程

在这里插入图片描述
自己设置工程名和路径,然后点击下一步。
在这里插入图片描述
Build system先选择qmake;然后一直点下一步,直到完成。
在这里插入图片描述在这里插入图片描述在这里插入图片描述

2.2 修改.pro文件

修改CSDN_Case.pro文件为:

QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++11

# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES += \
    main.cpp \
    mainwindow.cpp

HEADERS += \
    mainwindow.h

FORMS += \
    mainwindow.ui

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

INCLUDEPATH += $$PWD/../../../../../usr/local/include/vtk-9.0
DEPENDPATH += $$PWD/../../../../../usr/local/include/vtk-9.0


LIBS += -L$$PWD/../../../../../usr/local/lib/ \
-lvtkChartsCore-9.0 \
-lvtkCommonColor-9.0 \
-lvtkCommonComputationalGeometry-9.0 \
-lvtkCommonCore-9.0 \
-lvtkCommonDataModel-9.0 \
-lvtkCommonExecutionModel-9.0 \
-lvtkCommonMath-9.0 \
-lvtkCommonMisc-9.0 \
-lvtkCommonSystem-9.0 \
-lvtkCommonTransforms-9.0 \
-lvtkDICOM-9.0 \
-lvtkDICOMParser-9.0 \
-lvtkDomainsChemistry-9.0 \
-lvtkdoubleconversion-9.0 \
-lvtkexodusII-9.0 \
-lvtkexpat-9.0 \
-lvtkFiltersAMR-9.0 \
-lvtkFiltersCore-9.0 \
-lvtkFiltersExtraction-9.0 \
-lvtkFiltersFlowPaths-9.0 \
-lvtkFiltersGeneral-9.0 \
-lvtkFiltersGeneric-9.0 \
-lvtkFiltersGeometry-9.0 \
-lvtkFiltersHybrid-9.0 \
-lvtkFiltersHyperTree-9.0 \
-lvtkFiltersImaging-9.0 \
-lvtkFiltersModeling-9.0 \
-lvtkFiltersParallel-9.0 \
-lvtkFiltersParallelImaging-9.0 \
-lvtkFiltersPoints-9.0 \
-lvtkFiltersProgrammable-9.0 \
-lvtkFiltersSelection-9.0 \
-lvtkFiltersSMP-9.0 \
-lvtkFiltersSources-9.0 \
-lvtkFiltersStatistics-9.0 \
-lvtkFiltersTexture-9.0 \
-lvtkFiltersTopology-9.0 \
-lvtkFiltersVerdict-9.0 \
-lvtkfreetype-9.0 \
-lvtkGeovisCore-9.0 \
-lvtkgl2ps-9.0 \
-lvtkglew-9.0 \
-lvtkGUISupportQt-9.0 \
-lvtkGUISupportQtSQL-9.0 \
-lvtkhdf5-9.0 \
-lvtkhdf5_hl-9.0 \
-lvtkImagingColor-9.0 \
-lvtkImagingCore-9.0 \
-lvtkImagingFourier-9.0 \
-lvtkImagingGeneral-9.0 \
-lvtkImagingHybrid-9.0 \
-lvtkImagingMath-9.0 \
-lvtkImagingMorphological-9.0 \
-lvtkImagingSources-9.0 \
-lvtkImagingStatistics-9.0 \
-lvtkImagingStencil-9.0 \
-lvtkInfovisCore-9.0 \
-lvtkInfovisLayout-9.0 \
-lvtkInteractionImage-9.0 \
-lvtkInteractionStyle-9.0 \
-lvtkInteractionWidgets-9.0 \
-lvtkIOAMR-9.0 \
-lvtkIOAsynchronous-9.0 \
-lvtkIOCityGML-9.0 \
-lvtkIOCore-9.0 \
-lvtkIOEnSight-9.0 \
-lvtkIOExodus-9.0 \
-lvtkIOExport-9.0 \
-lvtkIOExportGL2PS-9.0 \
-lvtkIOExportPDF-9.0 \
-lvtkIOGeometry-9.0 \
-lvtkIOImage-9.0 \
-lvtkIOImport-9.0 \
-lvtkIOInfovis-9.0 \
-lvtkIOLegacy-9.0 \
-lvtkIOLSDyna-9.0 \
-lvtkIOMINC-9.0 \
-lvtkIOMotionFX-9.0 \
-lvtkIOMovie-9.0 \
-lvtkIONetCDF-9.0 \
-lvtkIOOggTheora-9.0 \
-lvtkIOParallel-9.0 \
-lvtkIOParallelXML-9.0 \
-lvtkIOPLY-9.0 \
-lvtkIOSegY-9.0 \
-lvtkIOSQL-9.0 \
-lvtkIOTecplotTable-9.0 \
-lvtkIOVeraOut-9.0 \
-lvtkIOVideo-9.0 \
-lvtkIOXML-9.0 \
-lvtkIOXMLParser-9.0 \
-lvtkjpeg-9.0 \
-lvtkjsoncpp-9.0 \
-lvtklibharu-9.0 \
-lvtklibproj-9.0 \
-lvtklibxml2-9.0 \
-lvtkloguru-9.0 \
-lvtklz4-9.0 \
-lvtklzma-9.0 \
-lvtkmetaio-9.0 \
-lvtknetcdf-9.0 \
-lvtkogg-9.0 \
-lvtkParallelCore-9.0 \
-lvtkParallelDIY-9.0 \
-lvtkpng-9.0 \
-lvtkpugixml-9.0 \
-lvtkRenderingAnnotation-9.0 \
-lvtkRenderingContext2D-9.0 \
-lvtkRenderingCore-9.0 \
-lvtkRenderingFreeType-9.0 \
-lvtkRenderingGL2PSOpenGL2-9.0 \
-lvtkRenderingImage-9.0 \
-lvtkRenderingLabel-9.0 \
-lvtkRenderingLOD-9.0 \
-lvtkRenderingOpenGL2-9.0 \
-lvtkRenderingQt-9.0 \
-lvtkRenderingSceneGraph-9.0 \
-lvtkRenderingUI-9.0 \
-lvtkRenderingVolume-9.0 \
-lvtkRenderingVolumeOpenGL2-9.0 \
-lvtkRenderingVtkJS-9.0 \
-lvtksqlite-9.0 \
-lvtksys-9.0 \
-lvtkTestingRendering-9.0 \
-lvtktheora-9.0 \
-lvtktiff-9.0 \
-lvtkverdict-9.0 \
-lvtkViewsContext2D-9.0 \
-lvtkViewsCore-9.0 \
-lvtkViewsInfovis-9.0 \
-lvtkViewsQt-9.0 \
-lvtkWrappingTools-9.0 \
-lvtkzlib-9.0 \

其中:

INCLUDEPATH += $$PWD/../../../../../usr/local/include/vtk-9.0
DEPENDPATH += $$PWD/../../../../../usr/local/include/vtk-9.0

1.DEPENDPATH

我们知道pro文件是Qt专有的项目配置文件,他的使用方式为,编译时先用qmake根据pro文件生成Makefile文件,再用make执行Makefile完成编译,那我们就看一下Makefile文件有啥区别

假如设置我们只有一个文件main.cpp要编译,DEPENDPATH = xxxx,xxxx里有两个文件dep.h和dep.cpp,对比后发现,加不加DEPENDPATH设置的区别如下:

没有DEPENDPATH设置
debug/main.o: ../../TestBoost/main.cpp 
    $(CXX) -c $(CXXFLAGS) $(INCPATH) -o debug\main.o ..\..\TestBoost\main.cpp
 
增加DEPENDPATH设置
debug/main.o: ../../TestBoost/main.cpp def.h
    $(CXX) -c $(CXXFLAGS) $(INCPATH) -o debug\main.o ..\..\TestBoost\main.cpp

根据makefile规则,紧跟在main.o: 后面的文件是编译生成main.o文件时依赖的文件,这个依赖的意思是,如果依赖的文件在main.o文件发生过更改,那么下次编译时main.o需要重新生成,否则无需重新生成

增加DEPENDPTH后,Qt会将该目录下所有的头文件加入到每个cpp文件对应的.o文件的依赖文件里,这样如果DEPENDPATH目录里的头文件发生了变化,所有的cpp都会重新编译
2.INCLUDEPATH

根据上述方式测试INCLUDEPATH,INCLUDEPATH 同样会被添加到编译.o文件的依赖文件中,不过INCLUDEPATH还有另一个作用就是,在代码里写#include时可以找到相应的文件,否则编译时会报找不到文件的错误。
————————————————
参考原文链接:https://blog.csdn.net/My__God/article/details/111517166

至于安装VTK生成的头文件目录,如果用的是默认的路径的话CMAKE_INSTALL_PREFIX = /usr/local,头文件就在/usr/local/include/vtk-9.0。不确定自己这里有没有可以进到这个目录下看一看。
安装VTK时BUILD_SHARED_LIBS = ON 编译生成的是动态链接库,位于/usr/local/lib
在这里插入图片描述

后面的一长串,是程序运行需要的链接的动态库。去掉对应库文件的.so后缀和前缀lib剩下的就是库名,然后在库名前加上-l(小写的L), -L后面跟的是链接库的路径,-l后面跟的是在程序中具体用到的库。这里是我为了一劳永逸,直接把该目录下所有的VTK链接库全带上了。

LIBS += -L$$PWD/../../../../../usr/local/lib/ \
-lvtkChartsCore-9.0 \
-lvtkCommonColor-9.0 \
...

到这里.pro文件就配置好了。

2.2 添加一个简单的VTK窗口显示案例程序

修改main.cpp:

#include "mainwindow.h"

#include <QApplication>

//本程序中使用到的VTK头文件
#include "vtkRenderWindow.h"
#include "vtkSmartPointer.h"

//VTK程序需要使用下面这两行代码来初始化,不然的话,运行程序啥都没有
#include "vtkAutoInit.h"
VTK_MODULE_INIT(vtkRenderingOpenGL2);
VTK_MODULE_INIT(vtkInteractionStyle);//这一行当前程序用不到,不起作用


int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    //直接在Qt自动生成的代码后添加VTK程序
    vtkSmartPointer<vtkRenderWindow> renWin = vtkSmartPointer<vtkRenderWindow>::New();//使用智能指针定义了一个类型为vtkRenderWindow的对象,这是VTK的类实例话对象的基本用法
    renWin->Render();//调用vtkRenderWindow里的方法显示并渲染一个VTK窗口
    std::cout<<"Hello World"<<std::endl;//只是测试
    std::cin.get();//没其他意义,只不过让程序暂停下来,等待接受用户的输入,目的是显示VTK窗口,防止VTK窗口一闪而过
    //return a.exec();
    return 0;
}

为了方便输入输出,在Qt左侧项目 ,点击run,在右侧的运行中选中 Run in Terminal.
在这里插入图片描述

2.3 构建、运行

输出结果,现在有三个窗口,一个Qt的,一个VTK的,一个终端用于输入输出。其实把Qt的窗口代码注释掉,不用显示Qt的窗口。
在这里插入图片描述如果到这一步都没啥问题,那就可以继续更加深入地使用VTK了。

三、使用CMAKE来编译VTK程序

3.1 新建一个测试文件夹,在这个文件夹中创建两个文件,newone.cpp 和 CMakeLists.txt.

newone.cpp

#include "vtkRenderWindow.h"
#include "vtkSmartPointer.h"
#include "vtkAutoInit.h"


VTK_MODULE_INIT(vtkRenderingOpenGL2);
VTK_MODULE_INIT(vtkInteractionStyle);
int main()
{

    vtkSmartPointer<vtkRenderWindow> renWin = vtkSmartPointer<vtkRenderWindow>::New();
    //vtkRenderWindow* renWin = vtkRenderWindow::New();
    renWin->Render();
    std::cout<<"Hello World"<<std::endl;


    std::cin.get();

    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 2.8)

PROJECT(newone)

find_package(VTK REQUIRED)

add_executable(newone MACOSX_BUNDLE newone.cpp)
INCLUDE_DIRECTORIES(/usr/local/include/vtk-9.0)

if(VTK_LIBRARIES)
  target_link_libraries(newone ${VTK_LIBRARIES})
else()
  target_link_libraries(newone 
vtkChartsCore-9.0
vtkCommonColor-9.0
vtkCommonComputationalGeometry-9.0
vtkCommonCore-9.0
vtkCommonDataModel-9.0
vtkCommonExecutionModel-9.0
vtkCommonMath-9.0
vtkCommonMisc-9.0
vtkCommonSystem-9.0
vtkCommonTransforms-9.0
vtkDICOM-9.0
vtkDICOMParser-9.0
vtkDomainsChemistry-9.0
vtkdoubleconversion-9.0
vtkexodusII-9.0
vtkexpat-9.0
vtkFiltersAMR-9.0
vtkFiltersCore-9.0
vtkFiltersExtraction-9.0
vtkFiltersFlowPaths-9.0
vtkFiltersGeneral-9.0
vtkFiltersGeneric-9.0
vtkFiltersGeometry-9.0
vtkFiltersHybrid-9.0
vtkFiltersHyperTree-9.0
vtkFiltersImaging-9.0
vtkFiltersModeling-9.0
vtkFiltersParallel-9.0
vtkFiltersParallelImaging-9.0
vtkFiltersPoints-9.0
vtkFiltersProgrammable-9.0
vtkFiltersSelection-9.0
vtkFiltersSMP-9.0
vtkFiltersSources-9.0
vtkFiltersStatistics-9.0
vtkFiltersTexture-9.0
vtkFiltersTopology-9.0
vtkFiltersVerdict-9.0
vtkfreetype-9.0
vtkGeovisCore-9.0
vtkgl2ps-9.0
vtkglew-9.0
vtkGUISupportQt-9.0
vtkGUISupportQtSQL-9.0
vtkhdf5-9.0
vtkhdf5_hl-9.0
vtkImagingColor-9.0
vtkImagingCore-9.0
vtkImagingFourier-9.0
vtkImagingGeneral-9.0
vtkImagingHybrid-9.0
vtkImagingMath-9.0
vtkImagingMorphological-9.0
vtkImagingSources-9.0
vtkImagingStatistics-9.0
vtkImagingStencil-9.0
vtkInfovisCore-9.0
vtkInfovisLayout-9.0
vtkInteractionImage-9.0
vtkInteractionStyle-9.0
vtkInteractionWidgets-9.0
vtkIOAMR-9.0
vtkIOAsynchronous-9.0
vtkIOCityGML-9.0
vtkIOCore-9.0
vtkIOEnSight-9.0
vtkIOExodus-9.0
vtkIOExport-9.0
vtkIOExportGL2PS-9.0
vtkIOExportPDF-9.0
vtkIOGeometry-9.0
vtkIOImage-9.0
vtkIOImport-9.0
vtkIOInfovis-9.0
vtkIOLegacy-9.0
vtkIOLSDyna-9.0
vtkIOMINC-9.0
vtkIOMotionFX-9.0
vtkIOMovie-9.0
vtkIONetCDF-9.0
vtkIOOggTheora-9.0
vtkIOParallel-9.0
vtkIOParallelXML-9.0
vtkIOPLY-9.0
vtkIOSegY-9.0
vtkIOSQL-9.0
vtkIOTecplotTable-9.0
vtkIOVeraOut-9.0
vtkIOVideo-9.0
vtkIOXML-9.0
vtkIOXMLParser-9.0
vtkjpeg-9.0
vtkjsoncpp-9.0
vtklibharu-9.0
vtklibproj-9.0
vtklibxml2-9.0
vtkloguru-9.0
vtklz4-9.0
vtklzma-9.0
vtkmetaio-9.0
vtknetcdf-9.0
vtkogg-9.0
vtkParallelCore-9.0
vtkParallelDIY-9.0
vtkpng-9.0
vtkpugixml-9.0
vtkRenderingAnnotation-9.0
vtkRenderingContext2D-9.0
vtkRenderingCore-9.0
vtkRenderingFreeType-9.0
vtkRenderingGL2PSOpenGL2-9.0
vtkRenderingImage-9.0
vtkRenderingLabel-9.0
vtkRenderingLOD-9.0
vtkRenderingOpenGL2-9.0
vtkRenderingQt-9.0
vtkRenderingSceneGraph-9.0
vtkRenderingUI-9.0
vtkRenderingVolume-9.0
vtkRenderingVolumeOpenGL2-9.0
vtkRenderingVtkJS-9.0
vtksqlite-9.0
vtksys-9.0
vtkTestingRendering-9.0
vtktheora-9.0
vtktiff-9.0
vtkverdict-9.0
vtkViewsContext2D-9.0
vtkViewsCore-9.0
vtkViewsInfovis-9.0
vtkViewsQt-9.0
vtkWrappingTools-9.0
vtkzlib-9.0
)
endif()

其中cmake_minimum_required 指定构建工程时所需的CMake版本需求;
PROJECT该命令指定工程名称;
find_package用于搜索并加载外部工程;
INCLUDE_DIRECTORIES用于确定头文件路径;
target_link_libraries用于指定生成可执行文件时需要链接哪些文件。

3.2 使用cmake编译

CMake是一种跨平台编译工具,比make更为高级,使用起来要方便得多。CMake主要是编写CMakeLists.txt文件,然后用cmake命令将CMakeLists.txt文件转化为make所需要的makefile文件,最后用make命令编译源码生成可执行程序或共享库(so(shared object))。因此CMake的编译基本就两个步骤:

  1. cmake
  2. make

cmake 指向CMakeLists.txt所在的目录,例如cmake .. 表示CMakeLists.txt在当前目录的上一级目录。cmake后会生成很多编译的中间文件以及makefile文件,所以一般建议新建一个新的目录,专门用来编译。在当前的源代码文件夹内,打开终端执行下面几行命令,编译生成可执行文件:

mkdir build
cd build
cmake ..
make

最后使用./newone执行可执行文件。出现下面的VTK窗口就算成功了,后面就可以自由发挥了。

/NewOne/build$ ./newone

在这里插入图片描述

四、遇到的问题(犯的错)

回忆从安装VTK开始,到第一个案例真的跑起来,我真的是遇到了很多的问题,差不多都快放弃了(其实我太垃圾了)。当时就想实在不行,还是换回OSG吧,至少OSG成功跑起来了。不过终于在今天凌晨一点把VTK成功弄好了,现在把这个过程中遇到的问题赶紧记录一下,不然以后又忘了。案例程序的配置过程我是依照《VTK图形图像开发进阶》这本书来的,但是它使用的版本太老,又都是在windows环境下,所以不能完全依照他的写法,但是程序部分还是可以参考的。

4.1 Qt中报错找不到头文件

error: vtkRenderWindow.h: No such file or directory
../CSDN_Case/main.cpp:4:10: fatal error: vtkRenderWindow.h: No such file or directory
    4 | #include "vtkRenderWindow.h"
      |          ^~~~~~~~~~~~~~~~~~~

在这里插入图片描述刚开始的时候不知道这两个头文件在哪,使用文件搜索功能,发现vtkRenderWindow.h在VTK的源代码目录下有,而且还有对应的定义cxx文件,在/usr/local/include/vtk-9.0这个目录下也有头文件。
在这里插入图片描述然后把INCLUDEPATH += $$PWD/../../../../../home/liukai/Reply/VTK/VTK-9.0.1/Rendering/Core/当成了头文件目录,仿造OSG的配置方法,把链接库配置如下,因为本案例程序从头文件名来看只用到了vtkRenderingCore-9.0 vtkCommonCore-9.0这两个库,所以就先这样配置了。

 LIBS += -L$$PWD/../../../../../usr/local/lib/ -lvtkRenderingQt-9.0 -lvtkRenderingCore-9.0 -lvtkCommonCore-9.0 -lpthread

INCLUDEPATH += $$PWD/../../../../../home/liukai/Reply/VTK/VTK-9.0.1/Rendering/Core/
DEPENDPATH += $$PWD/../../../../../home/liukai/Reply/VTK/VTK-9.0.1/Rendering/Core/
INCLUDEPATH += $$PWD/../../../../../home/liukai/Reply/VTK/VTK-9.0.1/Common/Core/
DEPENDPATH += $$PWD/../../../../../home/liukai/Reply/VTK/VTK-9.0.1/Common/Core/

然后在Qt中构建项目,结果:

../Reply/VTK/VTK-9.0.1/Common/Core/vtkIOStream.h:25: error: vtkConfigure.h: No such file or directory
In file included from ../../../Reply/VTK/VTK-9.0.1/Common/Core/vtkNew.h:47,
                 from ../../../Reply/VTK/VTK-9.0.1/Rendering/Core/vtkRenderWindow.h:44,
                 from ../CSDN_Case/main.cpp:4:
../../../Reply/VTK/VTK-9.0.1/Common/Core/vtkIOStream.h:25:10: fatal error: vtkConfigure.h: No such file or directory
   25 | #include "vtkConfigure.h"
      |          ^~~~~~~~~~~~~~~~

就会出现各种因为头文件中使用到的其他头文件未被包含,而找不到的错误。找到了这个文件,包含进来,下次又是其他的文件找不到,那要是把所有的问题解决我得包含多少路径,于是我开始意识到这种方法可能一开始就不对。于是把头文件的包含路径换回了/usr/local/include/vtk-9.0 ,把.pro文件修改成了下面这样:

INCLUDEPATH += $$PWD/../../../../../usr/local/include/vtk-9.0
DEPENDPATH += $$PWD/../../../../../usr/local/include/vtk-9.0

LIBS += -L$$PWD/../../../../../usr/local/lib/ -lvtkRenderingQt-9.0 -lvtkRenderingCore-9.0 -lvtkCommonCore-9.0 -lpthread

现在终于构建不报错了,但是运行VTK程序就是死活啥反应都没有其实这里是最接近成功的一次
百度VTK运行后没反应,就没找到相关的回答。然后想到了用调试来看看,到底有没有真正的执行VTK源码。但是调试F11到达VTK函数Render()和New()时,就只在头文件里打转,进不了具体的函数定义文件。(后来才发现貌似就算程序配置得没问题,也不会直接跳到cxx定义文件中,这里我猜测是使用链接动态库之后,具体的定义已经被写入了动态库当中,所以是不会直接进入定义的cxx文件中的,不知道这样理解对不对?有大佬知道的话可以评论区说一下)

当时还以为就是因为进不去定义的文件才导致啥也不显示出来,然后又把包含的头文件路径改成了包含头文件又包含定义的cxx文件路径,但还是不行,而且有错;结果绕来绕去把心态弄炸了。昨天终于狠下心花了点钱看了这篇VIP文章https://blog.csdn.net/dongdong_csdn/article/details/80877121,才幡然醒悟。

其实.pro文件就按之前的这个,但是还差一点,

INCLUDEPATH += $$PWD/../../../../../usr/local/include/vtk-9.0
DEPENDPATH += $$PWD/../../../../../usr/local/include/vtk-9.0

LIBS += -L$$PWD/../../../../../usr/local/lib/ -lvtkRenderingQt-9.0 -lvtkRenderingCore-9.0 -lvtkCommonCore-9.0 -lpthread

但是一定要在main.cpp中加上下面的代码,这两句是VTK初始化必须的初始化代码,不然什么都显示不出来,没反应。

#include "vtkAutoInit.h"
VTK_MODULE_INIT(vtkRenderingOpenGL2);//VTK was built with vtkRenderingOpenGL2

本来以为加上就可以了,结果:

:-1: error: main.o: in function `(anonymous namespace)::vtkRenderingOpenGL2_ModuleInit::vtkRenderingOpenGL2_ModuleInit()':
/Documents/Qt_WorkSpace/build-CSDN_Case-Desktop_Qt_5_15_2_GCC_64bit-Debug/../CSDN_Case/main.cpp:9: error: undefined reference to `vtkRenderingOpenGL2_AutoInit_Construct()'

在这里插入图片描述
心态再次炸裂。冷静下来之后,仔细想想,调用这种三方库,出现error: undefined reference to 多半都是由于没有包含对应的动态链接库,然后就老老实实的按那篇VIP文章中的一劳永逸,把所有的链接库都写进去,就可以了。
花了我两天两夜结果终于成功了。

4.2 CMake中遇到的问题,犯错

多数问题,在QT中解决之后,基本都明了了。关于CMake的使用,我主要参考的是这篇博客https://blog.csdn.net/baidu_38172402/article/details/80751312

五、总结

首先,自己菜,啥都不懂,对QT,CMAKE都不熟。
找到一份正确的参考资料很重要。
遇到问题一定要冷静,最好去问问大佬。

最后,如果文章中有什么问题的话,欢迎各位大佬在评论区指出。

VTK实现三维地质建模

前言

本文不适合0基础的VTK开发者,需要提前了解VTK可视化管线和渲染引擎的概念和流程,建议先阅读《VTK图形图像开发进阶》这本书稍微了解一下,并且根据里面的学习案例对VTK有一些了解。 VTK真的不是装好就能上手用的,相比之下OpenCV这种图像处理库真的好用。
本文主要参考文献【1】基于 VTK 和 QT 的层状地质体三维建模及可视化研究—姜弢、陈振振、徐学纯
【2】基于VTK技术的三维地层可视化研究_刘玉芳

目前,地质体三维数据模型总体上可分为线模型 、 表面模型 、 实体模型 、 面向对象的三维数据模型及混合数据模型五大类型 。 线模型的优点是集合关系明确,缺点是对于实体间关系表达及实现交 、并等叠加操作较困难 。 表面模型的优点是便于边界约束 、 显示和数据更新,缺点是空间分析难以进行 。实体模型的优点是便于空间操作和分析,缺点是占用空间较大,计算速度也较慢 。 面向对象的三维数据模型和混合数据模型分别综合了线 、 表面 、 实体模型的优缺点,但实现算法复杂 。*[1]基于 VTK 和 QT 的层状地质体三维建模及可视化研究—姜弢、陈振振、徐学纯*
本文采用表面模型和体模型混合建模的方式进行地质体三维建模 。采用面模型和体模型混合建模的原因是只有面模型不能够准确地描述层状地质体内部的信息,但是面模型可以对层状地质体的边界进行约束。其次,对每个地层顶面和底面用体模型进行填充,常用的体模型有四面体、三棱柱和六面体,其中四面体模型可以构造非常复杂的地层模型,但是由于四面体数据拓扑结构复杂,数据冗余量大,运算负荷大,对计算机的要求较高,故使用广义三棱柱进行填充。*[1]基于 VTK 和 QT 的层状地质体三维建模及可视化研究—姜弢、陈振振、徐学纯*

一、构建不规则三角网(TIN)

三棱柱构模原理是由上下两个不平行的两个TIN三角形面和三个竖直侧面四边形面所组成的空间单元。【2】
一般使用Delaunay三角剖分算法对空间中不规则的三维散点集合进行三角剖分。
VTK的vtkDelaunay2D类实现了二维三角剖分。该类的输入数据为一个vtkPointSet或其子类表示的三维空间点集,其输出是一个三角网格vtkPolyData数据。虽然输入的是三维数据,但是算法仅使用XY平面数据进行平面三角剖分,而忽略Z轴的数据。当然,也可以为vtkDelaunay2D设置投影变换从而在新的投影平面上进行三角剖分。

1.1 构建模拟的地层数据

由于手上没有实际测量得到的地层表面散点数据,故只能自己构建一些点来建立地层模型。
这里构建的散点都是规则的点,即确定地层表面在X和Y方向上的点数Nx,Ny,形成规则格网,即等间隔矩形网格,通过格网线之间的间隔来描述或定义,同一个坐标轴上格网线间隔值相同,不同轴向上的格网线间隔值可以不同。通过定义开始点及每个坐标方向上的增量来显示指定坐标。【2】。对于实际测量得到的地层表面数据,可以对其进行离散点格网化,就是对每一个岩层层面离散点进行格网剖分,每个数据点记录了剖分后格网点的平面坐标及高程,共三列,依次记录坐标X,坐标Y,高程Z。
使用插值算法将离散点,如反距离加权插值算法、克里金插值算、最小曲率方法等,具体的我也还没开始研究,因为现在手上没有实际的测量数据。

构建一层地层,包括上表面和下表面:

vtkSmartPointer<vtkPoints> points=vtkSmartPointer<vtkPoints>::New();
unsigned int GridSize_X = 10,GridSize_Y=10;//10*10个等间距的点,也可以使X和Y方向上的点间隔不同
unsigned int id=0,num=0;
double z=0;
//先生成上表面点数据,在生成下表面点数据
for(int i =0;i<2;i++)
{
	std::cout<<"level : "<<i<<endl;
	for (unsigned int x = 0; x < GridSize_X; x++)
	{
		for (unsigned int y = 0; y < GridSize_Y; y++)
		{
			z=i*(-5) + vtkMath::Random(0,1);
			id=points->InsertNextPoint(x, y, z);
		}
	}
	std::cout<<std::endl;
}
std::cout<<points->GetNumberOfPoints()<<std::endl;

把两个界面的数据用vtkPoints对象points存放。注意:顶面和底面数据点的x 和y值相同,不同的只有z值,通过Z值可以区分不同地层之间的距离以及是否存在尖灭。

1.2 Delaunay三角剖分

使用VTK的vtkDelaunay2D类实现了对地层表面散点数据二维三角网剖分,为什么使用二维?因为这里的顶面和底面数据点的x和y相同(其实就算不同也可以不用考虑Z,不过要分开两个面进行三角剖分),不同的是Z值,我们要的结果是把这些点数据相互连接生成三角形拓扑,而我们使用三角形作为三棱柱的两个三角面,故生成三角形实际与Z值高程无关,只需要使用x和y值就行,最后再连接起该点形成三角形.
。由于使用的是三棱柱作为体元,使用顶面中生成的三角形作为三棱柱体元的上三角形,对应底面中生成的三角形作为三棱柱体元的下三角形,把这两个三角形拓扑关系所对应的散点数据进行连接,即可构成一个三棱柱模型。
在这里插入图片描述
对于同一地层的顶面和底面数据点要分开进行Delaunay三角剖分,这里是在上下两个面的点数据X和Y相同的情况下,如果不分开进行三角剖分的话,可能只会对顶面或底面进行三角剖分;倘若是上下两个面的点数据X和Y不同的情况,那就更容易出问题了,因为Dealunay2D只对XY平面进行三角剖分,即算法此时可能会把上下两个面的点当成是一个面的,然后进行三角剖分,则上下两个面的点就连接成三角形了。

//分离地层的顶部和底部地层表面数据点,本质是拷贝数据点到ceil和bottom
vtkSmartPointer<vtkPoints> ceil = vtkSmartPointer<vtkPoints>::New();
for (int i = 0; i < GridSize_X*GridSize_Y; ++i)
{
	auto point = points->GetPoint(i);
	ceil->InsertNextPoint(point);
	cout<<"ceil	point = "<<point[0]<<","<<point[1]<<","<<point[2]<<endl;
}

vtkSmartPointer<vtkPoints> bottom = vtkSmartPointer<vtkPoints>::New();
for (int i = GridSize_X*GridSize_Y; i < points->GetNumberOfPoints(); ++i)
{
	auto point = points->GetPoint(i);
	bottom->InsertNextPoint(point);
	cout<<"bottom	point = "<<point[0]<<","<<point[1]<<","<<point[2]<<endl;
}
// Add the grid points to a polydata object
vtkSmartPointer<vtkPolyData> polydata=vtkSmartPointer<vtkPolyData>::New();
polydata->SetPoints(points);
vtkSmartPointer<vtkPolyData> ceil_polydata =
	vtkSmartPointer<vtkPolyData>::New();
ceil_polydata->SetPoints(ceil);
vtkSmartPointer<vtkPolyData> bottom_polydata =
	vtkSmartPointer<vtkPolyData>::New();
bottom_polydata->SetPoints(bottom);

// Triangulate the grid points
vtkSmartPointer<vtkDelaunay2D> ceil_delaunay =
	vtkSmartPointer<vtkDelaunay2D>::New();
ceil_delaunay->SetInputData(ceil_polydata);
ceil_delaunay->Update();

vtkSmartPointer<vtkDelaunay2D> bottom_delaunay =
	vtkSmartPointer<vtkDelaunay2D>::New();
bottom_delaunay->SetInputData(bottom_polydata);
bottom_delaunay->Update();

//上下三角网combine two poly data
vtkSmartPointer<vtkAppendPolyData> appendfilter = vtkSmartPointer<vtkAppendPolyData>::New();
appendfilter->AddInputConnection(ceil_delaunay->GetOutputPort());
appendfilter->AddInputConnection(bottom_delaunay->GetOutputPort());

vtkSmartPointer<vtkPolyDataMapper> meshMapper =
	vtkSmartPointer<vtkPolyDataMapper>::New();
meshMapper->SetInputConnection(appendfilter->GetOutputPort());

此时把meshMapper送入渲染引擎,便可以看到上下两个面三角剖分后的结果。
在这里插入图片描述
在这里插入图片描述

二、构建地质三棱柱体模型

1.生成三棱柱体元

目前按我查到的资料来看,构建三棱柱都需要用到插入点数据到vtkPoints对象时返回的ID值,id=points->InsertNextPoint(x, y, z); 返回该点在points中的索引。但是我比较迷惑,就是例如下面这种情况:

   unsigned int GridSize = 10;
for (unsigned int x = 0; x < GridSize; x++)
{
	for (unsigned int y = 0; y < GridSize; y++)
	{
		points->InsertNextPoint(x, y, vtkMath::Random(0,1));//返回该point的ID
	}
}
vtkSmartPointer<vtkWedge> wedge =
       vtkSmartPointer<vtkWedge>::New();

   //SetID的顺序必须是三棱柱的上三角形的三个点ID和下三角形的三个点ID
   wedge->GetPointIds()->SetId(0, 0);//第一个参数是三棱柱的6个点序号,第二个参数是vtkPoints对象中的点ID,就是插入是返回的那个
   wedge->GetPointIds()->SetId(1, 2);
   wedge->GetPointIds()->SetId(2, 20);
   wedge->GetPointIds()->SetId(3, 1);
   wedge->GetPointIds()->SetId(4, 3);
   wedge->GetPointIds()->SetId(5, 21);

注意:这里我比较迷惑的是为什么SetId()里的第二个参数就只用一个数字来表示ID就可以了,那系统是怎么确定这个数字代表的是哪个vtkPoints对象的点ID?经过我的实验发现,这个数字代表的ID貌似指定的是距离SetId()最近的在它之前插入vtkPoints对象的点的ID,那就类似与就近原则?那假设当前代码中有多个vtkPoints对象,我想利用前面几个vtkPoints对象的ID点来构建三棱柱怎么办?这个我暂时还没搞到答案,群里问了半天也没人回答,有大佬知道的可以评论一下,VTK的点ID管理,网上是真的找不到啥资料,这个问题折腾了我好久了。
而且由于这个问题没有得到解决,使用Delaunay2D生成的三角形我也没有办法知道每个三角形对应的点ID是多少,所以我构建三棱柱用的点ID就是vtkPoints对象中点插入后返回的ID。

这里我构建三棱柱就是按照这种“就近原则”,把构建三棱柱的代码紧跟在vtkPoints对象的点插入之后。ID是按点插入的顺序来确定的,构建三棱柱时需要注意,构造三棱柱的6个点的id都是我推理计算出来的。

/**
 * MakeWedge必须紧跟在地层表面数据生成或读入之后,然后利用其ID来构建三棱柱,同时对地层表面数据读入或生成的顺序有要求,先Y后X
 * 这个函数构建的是三棱柱集合
 * */
vtkSmartPointer<vtkUnstructuredGrid> MakeWedge(vtkSmartPointer<vtkPoints> points,unsigned int GridSize_X,unsigned int GridSize_Y)
{
	vtkSmartPointer<vtkUnstructuredGrid> ug =
        vtkSmartPointer<vtkUnstructuredGrid>::New();
    ug->SetPoints(points);

	vtkIdType p=0;//设置一个ID编号
	//每列GridSizeY-1个矩形格子,每行GridSizeX-1个矩形格子
	for(int i=1;i<GridSize_X;i++)//从矩形格子开始统计,一个矩形格子可以划分为两个三角形,
	{
		for(int j=1;j<GridSize_Y;j++)
		{
			//先构造矩形格网的左下三棱柱
			//SetID的顺序必须是三棱柱的上三角形的三个点ID和下三角形的三个点ID
			vtkSmartPointer<vtkWedge> wedge1 =
        		vtkSmartPointer<vtkWedge>::New();
			wedge1->GetPointIds()->SetId(0, p);
			wedge1->GetPointIds()->SetId(1, p+1);
			wedge1->GetPointIds()->SetId(2, p+GridSize_Y);
			//下三角形ID
			wedge1->GetPointIds()->SetId(3, GridSize_X*GridSize_Y+p);
			wedge1->GetPointIds()->SetId(4, GridSize_X*GridSize_Y+p+1);
			wedge1->GetPointIds()->SetId(5, GridSize_X*GridSize_Y+p+GridSize_Y);

			//添加三棱柱
			//if(p==4)
			ug->InsertNextCell(wedge1->GetCellType(), wedge1->GetPointIds());
			//先构造矩形格网的左下三棱柱
			vtkSmartPointer<vtkWedge> wedge2 =
        		vtkSmartPointer<vtkWedge>::New();
			wedge2->GetPointIds()->SetId(0, p+1);
			wedge2->GetPointIds()->SetId(1, p+GridSize_Y+1);
			wedge2->GetPointIds()->SetId(2, p+GridSize_Y);
			//下三角形ID
			wedge2->GetPointIds()->SetId(3, GridSize_X*GridSize_Y+p+1);
			wedge2->GetPointIds()->SetId(4, GridSize_X*GridSize_Y+p+GridSize_Y+1);
			wedge2->GetPointIds()->SetId(5, GridSize_X*GridSize_Y+p+GridSize_Y);
			//if(p==-3)
			ug->InsertNextCell(wedge2->GetCellType(), wedge2->GetPointIds());
			p++;
			if(j==GridSize_Y-1)//点ID到达矩形网格最上边界之后,直接跳转到下一个ID
				p++;
		}
	}
    return ug;
}

int main(int, char *[])
{
	vtkSmartPointer<vtkPoints> points=vtkSmartPointer<vtkPoints>::New();
	unsigned int GridSize_X = 10,GridSize_Y=10;
	unsigned int id=0,num=0;
	double z=0;
	//先生成上表面点数据,在生成下表面点数据
	for(int i =0;i<LEVEL;i++)
	{
		std::cout<<"level "<<i<<endl;
		for (unsigned int x = 0; x < GridSize_X; x++)
		{
			for (unsigned int y = 0; y < GridSize_Y; y++)
			{
				z=i*(-5) + vtkMath::Random(0,1);
				//z=(i-0.5)*(GridSize_X-x);
				id=points->InsertNextPoint(x, y, z);
			}
		}
		std::cout<<std::endl;
	}
	std::cout<<points->GetNumberOfPoints()<<std::endl;
	//构建三棱柱集合
	vtkSmartPointer<vtkUnstructuredGrid> wedgeArray=MakeWedge(points,GridSize_X,GridSize_Y);

到这里这一层地层的不规则三角形(TIN)面和三棱柱体元就建立好了。想要在窗口中显示出来,还需要配置VTK的可视化管线和渲染引擎。

vtkSmartPointer<vtkUnstructuredGrid> wedgeArray=MakeWedge(points,GridSize_X,GridSize_Y);
// Visualize
vtkSmartPointer<vtkNamedColors> WedgeColor =
	vtkSmartPointer<vtkNamedColors>::New();
//三棱柱映射
   vtkSmartPointer<vtkDataSetMapper> wedgeMapper = vtkSmartPointer<vtkDataSetMapper>::New();
   wedgeMapper->SetInputData(wedgeArray);
vtkSmartPointer<vtkActor> wedgeActor=vtkSmartPointer<vtkActor>::New();
   wedgeActor->SetMapper(wedgeMapper);
wedgeActor->GetProperty()->SetColor(WedgeColor->GetColor3d("Banana").GetData());
wedgeActor->GetProperty()->EdgeVisibilityOn();//显示三角形的边

vtkSmartPointer<vtkRenderer> renderer =
	vtkSmartPointer<vtkRenderer>::New();
renderer->AddActor(wedgeActor);//显示三棱柱体元
renderer->SetBackground(colors->GetColor3d("Mint").GetData());
vtkSmartPointer<vtkRenderWindow> renderWindow =
	vtkSmartPointer<vtkRenderWindow>::New();
renderWindow->AddRenderer(renderer);
vtkSmartPointer<vtkRenderWindowInteractor> renderWindowInteractor =
	vtkSmartPointer<vtkRenderWindowInteractor>::New();
renderWindowInteractor->SetRenderWindow(renderWindow);
renderWindow->Render();
renderWindowInteractor->Start();

在这里插入图片描述在这里插入图片描述

总结

我是第一次使用VTK建模,完成这一层地层建模就花了我差不多两个多星期,虽然完成了,但是还是有很多疑惑还是没有解决,在查找问题的过程中我也发现,CSDN上VTK对应的文章挺多的,但是都不怎么系统,遇到一些问题,想问问别人却几年都没有回应,VTK贴吧更是冷清,在QQ上找了个vtk开发群700多个人,在线的不到100,会帮忙回答问题的更是少之又少。这一点和OSG那边真的形成鲜明对比,OSG那边好像是有个专门的使用OSG开发的国内公司在管理,群里面也是他们在帮忙解答一些问题,而且群里人很多,问一个问题基本马上就有回复,还有出一些中文教程。唉,要是VTK也有这样的组织就好了。

C++11并发与多线程【第十节】future其他成员函数、shared_future、atomic

第十节 future其他成员函数、shared_future、atomic

学习视频

一、std::future的其他成员函数

std::future_status有三种状态

#include<iostream>
#include<vector>
#include<thread>
#include<string>
#include<list>
#include<mutex>
#include <future>

using namespace std;

//线程入口函数
int mythread(){
    cout<<"mythread() start "<<"thread id = "<<std::this_thread::get_id()<<endl;
    std::chrono::milliseconds dura(5000);//休息5秒钟
    std::this_thread::sleep_for(dura);//休息
    cout<<"mythread() end "<<"thread id = "<<std::this_thread::get_id()<<endl;
    return 5;
}

int main(){
    cout<<"main thread id = "<<std::this_thread::get_id()<<endl;
    std::future<int> result=std::async(std::launch::deferred,mythread);//自动创建一个新线程,使用mythread()作为入口函数
    cout<<"continue  ...."<<endl;

    std::future_status status = result.wait_for(std::chrono::seconds(6));//等待一秒
    if(status==std::future_status::timeout){//超时,表示异步创建的线程还没执行完
        cout<<"Time Out"<<endl;
    }
    else if(status==std::future_status::ready)//线程已经执行完成
    {    
        cout<<"THREAD RETURN Get Result"<<endl;
        cout<<result.get()<<endl;//获取线程的执行结果
    }
    else if(status==std::future_status::deferred){//如果async的第一个参数被设置为std::launch::deferred,此时延迟了,则本条件成立
        cout<<"THREAD Delay!!"<<endl;
        cout<<result.get()<<endl;//此时线程入口函数才会执行,此时不会创建新线程,而是都在主线程中执行
    }
    cout<<"i love china"<<endl;
    return 0;
}

二、std::shared_future

std::shared_future也是个类模板,std::future::get()使用移动语义转移数据获取值,只能获取一次,再用一次get()会报错,因为get()的内部是移动语义,直接把数据移出,然后就空了,而std::shared_future::get()复制数据。

#include<iostream>
#include<vector>
#include<thread>
#include<string>
#include<list>
#include<mutex>
#include <future>

using namespace std;

int mythread(int mypar){
    cout<<"args = "<<mypar<<endl;
    cout<<"mythread() start "<<"thread id = "<<std::this_thread::get_id()<<endl;
    std::chrono::milliseconds dura(5000);//休息5秒钟
    std::this_thread::sleep_for(dura);//休息
    cout<<"mythread() start "<<"thread id = "<<std::this_thread::get_id()<<endl;
    return 5;
}

void mythread2(std::shared_future<int> &tmpf){
    cout<<"mythread2() start  thread ID = "<<std::this_thread::get_id()<<endl;
    auto result =tmpf.get();//
    cout<<"mythread2() result = "<<result<<endl;
    return;
}

int main(){

    cout<<"main thread id = "<<std::this_thread::get_id()<<endl;
    std::packaged_task<int (int)> mypack(mythread);//packaged_task模板对象是int (int)即入口函数mythread的返回值和参数都是int,在这里把函数mythread通过packaged_task包装起来
    std::thread t1(std::ref(mypack),1);//线程直接开始执行,第二个参数作为线程入口函数的参数
    t1.join();//等待线程执行完毕
    std::future<int> result =mypack.get_future();//std::future对象里含有线程入口函数的返回结果,这里result保存mythread
    
    //std::shared_future<int> result_s(std::move(result));//
    bool ifcanget=result.valid();//可通过result.valid判断result数据是否还在
    std::shared_future<int> result_s(result.share());//两种写法意义相同,result.share()同样使用移动语义把数据移动到result_s
    ifcanget=result.valid();//此时ifcanget为false
    auto mythreadresult =result_s.get();
    mythreadresult=result_s.get();//可以执行多次get(),因为是复制数据

    std::thread t2(mythread2,std::ref(result_s));
    t2.join();
    return 0;
}

除此之外可以通过get_future返回值直接构造一个shared_future对象, std::shared_future<int> result_s(mypack.get_future());


三、原子操作std::atomic

3.1 原子操作引出范例

#include<iostream>
#include<vector>
#include<thread>
#include<string>
#include<list>
#include<mutex>
#include <future>

using namespace std;

int g_mycout=0;//定一个全局变量
std::mutex my_mutex;//互斥量
void mythread(){
    for(int i=0;i<10000;i++){
        //my_mutex.lock();//放在循环内
        g_mycout++;
        //my_mutex.unlock();
    }
    return;
}

int main(){

    thread mytobj1(mythread);
    thread mytobj2(mythread);
    mytobj1.join();
    mytobj2.join();
    cout<<"finally g_mycount == "<<g_mycout<<endl;
    return 0;
}

两个线程对同一个变量进行计算,如果不使用互斥量加锁的话,g_mycout 在加一的过程中可能会被另外一个线程打断,然后导致最后结果出错。

除了使用互斥量之外,还可以使用原子操作来解决。使用原子操作的效率要高于互斥量加锁解锁的方式,例如在上面的例子中,互斥量在循环内意味着要执行10000次加锁解锁,两个线程的话就是20000次。

原子操作可以理解成一种:不需要使用互斥量加锁(无锁)技术的多线程并发编程方式。
原子操作是在多线程中不会被打断的程序执行片段。原子操作在单行代码上,比互斥量在效率上高。互斥量的作用范围一般是一段代码。

原子操作一般指的是不可分割的操作,这种操作要么是完成的,要么是未完成的。

std::atomic 代表原子操作,是个类模板,用来封装某个类型的值。

using namespace std;

std::atomic<int> g_mycout(0);//封装了一个类型为int的对象,这是一个可以执行原子操作的整型变量
//std::mutex my_mutex;//互斥量
void mythread(){
    for(int i=0;i<10000;i++){
        //my_mutex.lock();//放在循环内
        g_mycout++;//现在的操作是原子操作,不会被打断
        //my_mutex.unlock();
    }
    return;
}

int main(){

    thread mytobj1(mythread);
    thread mytobj2(mythread);
    mytobj1.join();
    mytobj2.join();
    cout<<"finally g_mycount == "<<g_mycout<<endl;
    return 0;
}

其他案例:

std::atomic<bool> g_if_end(false);//线程退出标记,执行原子操作的bool类型变量
void mythread(){
    std::chrono::milliseconds dura(1000);//一秒钟
    while(g_if_end==false){
        //系统没要求退出,线程可以干自己的工作
        cout<<"运行中 thread ID = "<<std::this_thread::get_id()<<endl;
        std::this_thread::sleep_for(dura);
    }
    cout<<"thread ID = "<<std::this_thread::get_id()<<"结束"<<endl;
}
int main(){

    thread mytobj1(mythread);
    thread mytobj2(mythread);
    std::chrono::milliseconds dura(5000);
    std::this_thread::sleep_for(dura);
    g_if_end=true;//对原子对象的写操作,让线程结束
    mytobj1.join();
    mytobj2.join();
    cout<<"程序执行完毕"<<endl;
    
    return 0;
}

运行中 thread ID = 140737348093696
运行中 thread ID = 140737339700992
运行中 thread ID = 140737348093696
运行中 thread ID = 140737339700992
运行中 thread ID = 140737348093696
运行中 thread ID = 140737339700992
运行中 thread ID = 140737348093696
运行中 thread ID = 140737339700992
运行中 thread ID = 140737348093696
运行中 thread ID = 140737339700992
thread ID = 140737348093696结束
thread ID = 140737339700992结束
程序执行完毕

3.2 总结

原子操作一般用于计数或统计(累计发出去了多少数据包,或接受)。

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

c++11并发与多线程【第九节】async、 future、packaged_task、promise

第九节 async、 future、packaged_task、promise

本课程视频链接:https://www.bilibili.com/video/BV1Yb411L7ak?p=11

一、std::async、std::future创建后台任务并返回值,希望线程返回一个结果

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

1.1 什么叫启动一个异步任务?

自动创建一个线程并开始执行对应的入口函数,它返回一个std::future对象,这个std::future对象里就含有线程入口函数所返回的结果(即线程返回的结果),我们可以通过调用std::future对象的成员函数get()来获取结果。

future: 将来,也有人称呼std::future提供了一种访问异步操作结果的机制。就是说这个结果你可能没有办法马上拿到,但是不久的将来,线程执行结束之后,就可以拿到结果。即future对象中会保存一个值,在将来的某个时刻能够拿到。

使用std::asyncstd::future之前需要包含头文件#include<future>

#include<iostream>
#include<vector>
#include<thread>
#include<string>
#include<list>
#include<mutex>
#include <future>

using namespace std;

//线程入口函数
int mythread(){
    cout<<"mythread() start "<<"thread id = "<<std::this_thread::get_id()<<endl;
    std::chrono::milliseconds dura(5000);//休息5秒钟
    std::this_thread::sleep_for(dura);//休息
    cout<<"mythread() start "<<"thread id = "<<std::this_thread::get_id()<<endl;
    return 5;
}

int main(){
    cout<<"main thread id = "<<std::this_thread::get_id()<<endl;
    std::future<int> result=std::async(mythread);//自动创建一个新线程,使用mythread()作为入口函数
    cout<<"continue  ...."<<endl;
    cout<<result.get()<<endl;//get()取的是将来的值,mythread还没有执行完,所以主线程会阻塞在这里等待mythread()执行完毕
    cout<<"i love china"<<endl;
    return 0;
}

该程序通过std::future对象的get()成员函数等待线程执行结束并返回结果;
get()函数在拿到返回值之前会一直阻塞。同样的还有future的成员函数wait(),区别在于wait()只等待线程返回,但是不获取返回值。
注意:future::get()只能调用一次。

1.2 使用类成员函数作为可调用对象

class A
{
public:
    //线程入口函数
    int mythread(int mypar){
        cout<<"args = "<<mypar<<endl;
        cout<<"mythread() start "<<"thread id = "<<std::this_thread::get_id()<<endl;
        std::chrono::milliseconds dura(5000);//休息5秒钟
        std::this_thread::sleep_for(dura);//休息
        cout<<"mythread() start "<<"thread id = "<<std::this_thread::get_id()<<endl;
        return 5;
    }
};


int main(){
    A a;
    int tempar=12;
    cout<<"main thread id = "<<std::this_thread::get_id()<<endl;
    std::future<int> result=std::async(&A::mythread,&a,tempar);//自动创建一个新线程,使用mythread()作为入口函数,第二个参数是对象引用,才能保证创建线程里的对象和主线程是同一个对象
    cout<<"continue  ...."<<endl;
    cout<<result.get()<<endl;//get()取的是将来的值,mythread还没有执行完,所以主线程会阻塞在这里等待mythread()执行完毕
    //result.wait();
    cout<<"i love china"<<endl;
    return 0;
}

其中自动创建一个新线程,使用mythread()作为入口函数,第二个参数是对象引用,才能保证创建线程里的对象和主线程是同一个对象。

std::future<int> result=std::async(&A::mythread,&a,tempar);

1.3 std::launch类型

通过额外向std::async()传递一个参数,该参数类型的std::lunnch类型(枚举类型),来达到一些目的。

  1. std::launch::deferred 表示线程入口函数调用被延迟到std::future的wait()或者get()函数调用时才执行。
    当get()或wait()没有被调用时,线程会执行吗?std::future<int> result=std::async(std::launch::deferred,&A::mythread,&a,tempar); 作为第一个参数。

实际上,如果get()或wait()没有被调用,这个线程根本就不会被创建,更不会执行该入口函数。
如果get()或wait()被调用了并且也不会创建一个新线程,实际上执行该入口函数的还是主线程。

int main(){
    A a;
    int tempar=12;
    cout<<"main thread id = "<<std::this_thread::get_id()<<endl;
    std::future<int> result=std::async(std::launch::deferred,&A::mythread,&a,tempar);//自动创建一个新线程,使用mythread()作为入口函数,第二个参数是对象引用,才能保证创建线程里的对象和主线程是同一个对象
    cout<<"continue  ...."<<endl;
    cout<<result.get()<<endl;//get()取的是将来的值,mythread还没有执行完,所以主线程会阻塞在这里等待mythread()执行完毕
    //result.wait();
    cout<<"i love china"<<endl;
    return 0;
}

两个线程ID都是主线程的ID:

main thread id = 140737348097856
continue ….
args = 12
mythread() start thread id = 140737348097856
mythread() start thread id = 140737348097856
5
i love china

  1. std::launch::async 在调用async函数时就创建新线程;
int main(){
    A a;
    int tempar=12;
    cout<<"main thread id = "<<std::this_thread::get_id()<<endl;
    std::future<int> result=std::async(std::launch::async,&A::mythread,&a,tempar);//自动创建一个新线程,使用mythread()作为入口函数,第二个参数是对象引用,才能保证创建线程里的对象和主线程是同一个对象
    cout<<"continue  ...."<<endl;
    cout<<result.get()<<endl;//get()取的是将来的值,mythread还没有执行完,所以主线程会阻塞在这里等待mythread()执行完毕
    //result.wait();
    cout<<"i love china"<<endl;
    return 0;
}

执行结果:两个线程ID不同

main thread id = 140737348097856
continue ….
args = 12
mythread() start thread id = 140737348093696
mythread() start thread id = 140737348093696
5
i love china

如果同时加上std::launch::async|std::launch::deferred作为参数,不论顺序,均以async为最终取值。

二、std::packaged_task:打包任务,把任务包装起来

2.1std::packaged_task是个类模板,它的模板参数是各种可调用对象。通过std::packaged_task来把各种可调用对象包装起来,方便将来作为线程入口函数.

int mythread(int mypar){
    cout<<"args = "<<mypar<<endl;
    cout<<"mythread() start "<<"thread id = "<<std::this_thread::get_id()<<endl;
    std::chrono::milliseconds dura(5000);//休息5秒钟
    std::this_thread::sleep_for(dura);//休息
    cout<<"mythread() start "<<"thread id = "<<std::this_thread::get_id()<<endl;
    return 5;
}

int main(){

    cout<<"main thread id = "<<std::this_thread::get_id()<<endl;
    std::packaged_task<int (int)> mypack(mythread);//packaged_task模板对象是int (int)即入口函数mythread的返回值和参数都是int,在这里把函数mythread通过packaged_task包装起来
    std::thread t1(std::ref(mypack),1);//线程直接开始执行,第二个参数作为线程入口函数的参数
    t1.join();//等待线程执行完毕
    std::future<int> result =mypack.get_future();//std::future对象里含有线程入口函数的返回结果,这里result保存mythread
    cout<<result.get()<<endl;
    return 0;
}

packaged_task模板对象是int (int)即入口函数mythread的返回值和参数都是int,在这里把函数mythread通过packaged_task包装起来.

也可使用lambda表达式达到相同的效果:

int main(){

    cout<<"main thread id = "<<std::this_thread::get_id()<<endl;
    // std::packaged_task<int (int)> mypack(mythread);//packaged_task模板对象是int (int)即入口函数mythread的返回值和参数都是int,在这里把函数mythread通过packaged_task包装起来
    //使用lambda表达式作为入口函数,把入口函数的操作放在下方lambda表达式中
    std::packaged_task<int(int)> mypack([](int mypar){
        cout<<"args = "<<mypar<<endl;
        cout<<"mythread() start "<<"thread id = "<<std::this_thread::get_id()<<endl;
        std::chrono::milliseconds dura(5000);//休息5秒钟
        std::this_thread::sleep_for(dura);//休息
        cout<<"mythread() start "<<"thread id = "<<std::this_thread::get_id()<<endl;
        return 5;
    });
    std::thread t1(std::ref(mypack),1);//线程直接开始执行,第二个参数作为线程入口函数的参数
    t1.join();//等待线程执行完毕
    std::future<int> result =mypack.get_future();//std::future对象里含有线程入口函数的返回结果,这里result保存mythread线程的返回值
    cout<<result.get()<<endl;
    return 0;
}

2.2 packaged_task包装起来的可调用对象还可以直接调用,所以从这个角度来说,packaged_task对象也是一个可调用对象。

int main(){

    cout<<"main thread id = "<<std::this_thread::get_id()<<endl;
    // std::packaged_task<int (int)> mypack(mythread);//packaged_task模板对象是int (int)即入口函数mythread的返回值和参数都是int,在这里把函数mythread通过packaged_task包装起来


    //使用lambda表达式作为入口函数
    std::packaged_task<int(int)> mypack([](int mypar)
    {
        cout<<"args = "<<mypar<<endl;
        cout<<"mythread() start "<<"thread id = "<<std::this_thread::get_id()<<endl;
        std::chrono::milliseconds dura(5000);//休息5秒钟
        std::this_thread::sleep_for(dura);//休息
        cout<<"mythread() start "<<"thread id = "<<std::this_thread::get_id()<<endl;
        return 5;
    });
    mypack(105);//直接调用lambda表达式,相当于函数调用,并不会再创建线程
    std::future<int> result =mypack.get_future();
    cout<<result.get()<<endl;
    return 0;
}

此时不会创建新线程,线程ID都相同:

main thread id = 140737348097856
args = 105
mythread() start thread id = 140737348097856
mythread() start thread id = 140737348097856
5

2.3 其他用法

vector<std::packaged_task<int(int)>> mytasks;//创建一个容器
int main(){

    cout<<"main thread id = "<<std::this_thread::get_id()<<endl;

    //使用lambda表达式作为入口函数
    std::packaged_task<int(int)> mypack([](int mypar)
    {
        cout<<"args = "<<mypar<<endl;
        cout<<"mythread() start "<<"thread id = "<<std::this_thread::get_id()<<endl;
        std::chrono::milliseconds dura(5000);//休息5秒钟
        std::this_thread::sleep_for(dura);//休息
        cout<<"mythread() start "<<"thread id = "<<std::this_thread::get_id()<<endl;
        return 5;
    });

    mytasks.push_back(std::move(mypack));//直接把mypack移动到容器中,避免复制过程出错,移动之后mypack就为空
    std::packaged_task<int (int)> mypack2;
    auto iter=mytasks.begin();
    mypack2=std::move(*iter);//用mypack来接着容器中的对象,使用移动语义move    
    mytasks.erase(iter);//删除第一个元素,此时迭代已经失效了,后续不能再使用iter
    mypack2(123);
    std::future<int> result=mypack2.get_future();
    cout<<result.get()<<endl;
    return 0;
}

同样的,使用的都是主线程。

main thread id = 140737348097856
args = 123
mythread() start thread id = 140737348097856
mythread() start thread id = 140737348097856
5

备注:关于这个packaged_task的用法,具体的使用场景老师也没怎么说,不懂的可以自己再查找相关资料,实在不懂以后别乱用就行。

三、std::promise 类模板

我们能够在某个线程中给他赋值,然后在将来某个时刻可以在其他线程中拿出来用.
总结:通过promise保存一个值,在将来某一个时刻我们通过把一个future绑定到这个promise上来得到这个绑定的值。

std::promise<int> myprom;//声明一个promise对象,保存的值类型为int
std::thread t1(mythread,std::ref(myprom),180);
t1.join();//join和thread要配套使用
//获取结果值
std::future<int> ful=myprom.get_future();//promise和future绑定,用于获取线程返回值
auto result=ful.get();//get()只能使用一次
cout<<"result== "<<result<<endl<<" i love china"<<endl;

完整代码:

void mythread(std::promise<int> &temp, int calc)//
{
    //做一些操作
    calc++;
    calc *=10;
    //其他运算
    std::chrono::milliseconds dura(5000);//休息5秒钟
    std::this_thread::sleep_for(dura);//休息
    //计算出结果之后
    int result=calc;//保存结果
    temp.set_value(result);//结果保存到temp这个对象中
}
int main(){

    cout<<"main thread id = "<<std::this_thread::get_id()<<endl;
    std::promise<int> myprom;//声明一个promise对象,保存的值类型为int
    std::thread t1(mythread,std::ref(myprom),180);
    t1.join();
    //获取结果值
    std::future<int> ful=myprom.get_future();//promise和future绑定,用于获取线程返回值
    auto result=ful.get();//get()只能使用一次
    cout<<"result== "<<result<<endl<<" i love china"<<endl;
    return 0;
}

3.1 使用promise在不同线程传递结果

void mythread(std::promise<int> &temp, int calc)//
{
    //做一些操作
    calc++;
    calc *=10;
    //其他运算
    std::chrono::milliseconds dura(5000);//休息5秒钟
    std::this_thread::sleep_for(dura);//休息
    //计算出结果之后
    int result=calc;//保存结果
    temp.set_value(result);//结果保存到temp这个对象中
}

void mythread2(std::future<int> &temp){
    auto result=temp.get();
    cout<<"mythread2 result = "<<result<<endl;
}
int main(){
    cout<<"main thread id = "<<std::this_thread::get_id()<<endl;

    std::promise<int> myprom;//声明一个promise对象,保存的值类型为int
    std::thread t1(mythread,std::ref(myprom),180);
    t1.join();
    //获取结果值
    std::future<int> ful=myprom.get_future();//promise和future绑定,用于获取线程返回值
    //auto result=ful.get();//get()只能使用一次

    std::thread t2(mythread2,std::ref(ful));//即把线程1中的结果拿到线程2中使用
    t2.join();//等待线程2执行完毕
    cout<<" i love china"<<endl;
    return 0;
}

main thread id = 140737348097856
mythread2 result = 1810
i love china

四、总结

学习多线程相关的这些用法,并不是都要用在实际的项目中。
最佳的解决方法其实是用最少最简单的代码写出一个稳定、高效的多线程程序。
编码的过程中,可以多阅读一些高手写的代码,变成自己的积累。
学习这些东西的理由,实际上是为将来一些高手写的代码铺路。

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

(2021)Ubuntu环境下OSG的编译、安装与使用(vs code/QT)

前言

最近在做项目,学长推荐使用OSG来做三维可视化开发,我也是第一次接触这个东西,写个博客记录下过程,以免过几天又忘记了。

一、OSG的安装过程

OSG的安装过程主要依据这篇文章,OSG、osgQT编译及配置
其中osgQt的链接我这打不开,暂时也还没用到就没管了。

阅读更多...

c++11并发与多线程【第八节】condition_variable 、wait notify_one 、notify_all

第八节 condition_variable 、wait notify_one 、notify_all

一、条件变量std::condition_variable、wait、notify_one()

线程A:等待一个条件满足
线程B:专门往消息队列中放消息(数据)

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

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

    bool MsgProcess(int &command){
        //双重锁定,双重检查,避免每一次都要锁了之后再判断
        if(!msgRecvQueue.empty()){
            std::unique_lock<std::mutex> sbguard(my_mutex);
            if(!msgRecvQueue.empty()){
                command=msgRecvQueue.front();//返回第一个元素
                msgRecvQueue.pop_front();//取出后移除该元素
                //然后处理数据
                return true;
            }
        }
        return false;
    }
    //从消息队列list中读取玩家命令的线程入口函数
    void outMsgRecvQueue(){
        int command=0;
        for(int i=0;i<10000;i++){
            //
            bool result=MsgProcess(command);
            if(result){
                cout<<"outMsgRecvQueue执行,取出一个元素 "<<command<<endl;
                //然后对数据进行处理
            }
            else
                cout<<"outMsgRecvQueue执行,但是list已经空了 : "<<i<<endl;
        }
        cout<<"end "<<endl;
    }

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

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

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

其中,每一次取数据都需要判断消息队列是否为空,效率低,能不能让队列不为空时自动通知我们来取数据??

bool MsgProcess(int &command){
    //双重锁定,双重检查,避免每一次都要锁了之后再判断
    if(!msgRecvQueue.empty()){
        std::unique_lock<std::mutex> sbguard(my_mutex);
        if(!msgRecvQueue.empty()){
            command=msgRecvQueue.front();//返回第一个元素
            msgRecvQueue.pop_front();//取出后移除该元素
            //然后处理数据
            return true;
        }
    }
    return false;
}

1.1 使用条件变量std::condition_variable

std::condition_variable 实际上是一个与条件相关的类,说白了就是等待条件达成,需要与互斥量来配合工作。

class A{
public:
    //把收到的消息(玩家命令)放入到一个队列的线程入口函数
    void inMsgRecvQueue(){
        for(int i=0;i<10000;i++){//用数字模拟玩家发送来的命令
            cout<<"inMsgRecvQueue()执行,插入一个元素 "<<i<<endl;
            //
            std::unique_lock<std::mutex> sbguard(my_mutex);
            msgRecvQueue.push_back(i);//把命令放入队列当中
            //如果outMsgRecvQueue()取出数据线程正在处理一个事物,需要一段时间,而不是恰好卡在wait那等待唤醒,此时notify_one就没有效果
            my_cond.notify_one();//尝试唤醒wait()
            //
        }
    }

    //从消息队列list中读取玩家命令的线程入口函数
    void outMsgRecvQueue(){
        int command=0;
        while(true){
            std::unique_lock<std::mutex> sbguard(my_mutex);//在构造函数中给my_mutex上锁

            //wait()用来等一个信号,如果第二个参数(lambda表达式)返回值是false,那么wait()将解锁互斥量,并将本线程阻塞到本行,
            //阻塞直到某个其他线程调用notify_one()成员函数为止
            //如果wait第二个参数是true,wait()就直接返回
            //如果wait没有第二个参数,那么就和第二个参数返回false的效果相同
            //当其他线程用notify_one把本wait()唤醒,wait就开始恢复干活:
            //a)wait()不断尝试重新获取互斥锁,如果获取不到流程就继续阻塞到这等待获取,如果获取到了,加上锁就继续往下执行;
            //b.1)然后如果wait有第二个参数lambda,就判断这个lambda表达式,如果还是false,那么wait就又对互斥量解锁,然后又将本线程阻塞到本行,即回到第一个步骤
            //b.2)如果lambda表达式为true,则wait返回,程序往下执行,此时互斥量依然是被本线程锁着的
            //b.3)如果wait()没有第二个参数,则wait返回,程序往下执行,此时互斥量依然是被本线程锁着的
            my_cond.wait(sbguard,[this]{//lambda表达式,一个lambda表达式就是一个可调用对象(函数)
                if(!msgRecvQueue.empty())
                    return true;
                return false;
            });
            //程序如果能执行到这一步,互斥锁一定还是被本线程锁着的,消息队列中至少是有一条数据的
            command=msgRecvQueue.front();//返回第一个元素
            msgRecvQueue.pop_front();//取出后移除该元素
            sbguard.unlock();//由于unique_lock的灵活性,可以随时unlock解锁,以免锁住的时间太长
            cout<<"outMsg执行,取出一个元素"<<endl;
            //然后可以执行一些其他动作,如一些比较费时的操作
            //******
            //执行100毫秒
        }
        cout<<"end "<<endl;
    }

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

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

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

其中;

my_cond.wait(sbguard,[this]{//lambda表达式,一个lambda表达式就是一个可调用对象(函数)
                if(!msgRecvQueue.empty())
                    return true;
                return false;
            });

**wait()**用来等一个信号,如果第二个参数(lambda表达式)返回值是false,那么wait()将解锁互斥量,并将本线程阻塞到本行;
阻塞直到某个其他线程调用notify_one()成员函数为止;
如果wait第二个参数是true,wait()就直接返回;
如果wait没有第二个参数,那么就和第二个参数返回false的效果相同;
当其他线程用notify_one把本wait()唤醒,wait就开始恢复干活:
a)wait()不断尝试重新获取互斥锁,如果获取不到流程就继续阻塞到这等待获取,如果获取到了,加上锁就继续往下执行;
b.1)然后如果wait有第二个参数lambda,就判断这个lambda表达式,如果还是false,那么wait就又对互斥量解锁,然后又将本线程阻塞到本行,即回到第一个步骤;
b.2)如果lambda表达式为true,则wait返回,程序往下执行,此时互斥量依然是被本线程锁着的;
b.3)如果wait()没有第二个参数,则wait返回,程序往下执行,此时互斥量依然是被本线程锁着的;

注意:如果outMsgRecvQueue()线程正在处理一个事物,需要一段时间,而不是恰好卡在wait那等待唤醒,此时notify_one就没有效果。

二、对上述代码思考

思考写入数据的线程如果在取出数据线程之前执行的情况。

三、notify_all()

notify_one()只能通知一个线程,多线程时唤醒哪一个线程是不确定的。
如果多个线程干的活不一样,但是都被卡在wait(),此时就需要全部唤醒在wait()那卡住的线程。
在上面的例子中,就算把notify_one换成notify_all其实效果是一样的,因为wait唤醒后,还需要得到互斥锁,但是一次只能有一个线程得到互斥锁,所以并不会冲突或发生错误,当然仅限于本案例。

思考:notify_all()的应用场景

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

c++11并发与多线程【第七节】单例设计模式共享数据分析、解决、call_once

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

一、概谈设计模式

“设计模式”:代码的一些写法,程序灵活,维护起来比较方便,但是别人去接管代码,就比较困难;
设计模式不适合啥项目都往上面套,这就本末倒置了;

二、单例设计模式

单例设计模式一般的使用频率较高;
单例:整个项目中,有某个或者某些特殊的类,属于该类的对象,我只能创建一个,多了创建不了;

使用场景:在一个项目中,一个类由于各种原因只能生成一个类对象;

class MyCAS//单例类创建
{
private:
    MyCAS(){}//构造函数私有化,之后不能直接使用 MyCAS A; 这种方法创建对象

private:
    static MyCAS*m_instance;//静态成员变量,MyCAS类指针
public:
    static MyCAS *GetInstance(){
        if(m_instance==NULL)
            m_instance=new MyCAS();//在这创建单例对象
        return m_instance;
    }
    void func(){
        cout<<"测试"<<endl;
    }
    ~MyCAS(){
    	
    }
};
//类静态变量成员初始化
MyCAS *MyCAS::m_instance=NULL;

int main(){
    MyCAS *p_a=MyCAS::GetInstance();//主线程中创建一个单例对象,返回该类对象的指针;
    MyCAS *p_b=MyCAS::GetInstance();//此时实际上返回的还是上一个对象的指针,因此不会再创建一个对象

    return 0;
}

如何释放new 的MyCAS对象?
这里使用的是在类中套类,释放对象的一种方法,这种方法有啥好处?老师没说;
除此之外还可以在析构函数中释放;

class MyCAS//单例类创建
{
private:
    MyCAS(){}//构造函数私有化,之后不能直接使用 MyCAS A; 这种方法创建对象

private:
    static MyCAS*m_instance;//静态成员变量,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 func(){
        cout<<"测试"<<endl;
    }

};
//类静态变量成员初始化
MyCAS *MyCAS::m_instance=NULL;

int main(){
    MyCAS *p_a=MyCAS::GetInstance();//创建一个对象,返回该类对象的指针;
    MyCAS *p_b=MyCAS::GetInstance();//此时实际上返回的还是上一个对象的指针,因此不会再创建一个对象
    return 0;
}

三、单例设计模式—-多线程下共享数据问题分析、解决

问题:需要在自己创建的线程,即非主线程,中来创建MyCAS这个单例类的对象,这种线程可能不止一个;
于是就可能面临GetInstance() 这种成员函数需要互斥;

void *mythread(){
    cout<<"我的线程开始执行了"<<endl;
    MyCAS *p_a=MyCAS::GetInstance();
    cout<<"我的线程结束了"<<endl;
    return;
}

int main(){
    std::thread mytobj1(mythread);//这两个线程都以mythread为入口函数,
    std::thread mytobj2(mythread);//这是两个线程,即这里会有两条通路同时执行GetInstance()这个函数
    mytobj1.join();
    mytobj2.join();

    return 0;
}

这两个线程都以mythread为入口函数,这是两个线程,即这里会有两条通路同时执行GetInstance()这个函数,于是便有可能,当线程一刚刚执行到:if(m_instance==NULL),马上就被切换到线程二,线程二由于m_instance还是NULL,也会满足if条件,继续执行下一步m_instance=new MyCAS();导致出现多个MyCAS对象,不满足单例条件

解决:使用互斥量,来保证互斥访问GetInstance();

std::mutex my_mutex;//创建一个互斥量
class MyCAS//单例类创建
{
private:
    MyCAS(){}//构造函数私有化,之后不能直接使用 MyCAS A; 这种方法创建对象

private:
    static MyCAS*m_instance;//静态成员变量,MyCAS类指针
public:
    static MyCAS *GetInstance(){

        std::unique_lock<std::mutex> mymutex(my_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 func(){
        cout<<"测试"<<endl;
    }
};

但是使用这种方法,std::unique_lock<std::mutex> mymutex(my_mutex);//自动加锁 效率比较低,只是解决第一次加锁的问题,实际上第一个线程执行加锁new了一个对象之后,后面的线程都不需要在经历加锁再判断这个流程;

使用双重锁定,可以优化:N个线程调用GetInstance()函数之后,一个或者小于N个线程(小于N的情况可能都比较少,这种情况是假设一个线程在执行完第一个if(m_instance==NULL)之后,马上又切换到其他线程)的m_instance是NULL,但是有且仅有一个线程可以加锁 且new对象,等他退出之后,其他线程进入这个函数之后m_instance已经不等于NULL,就不需要在经历加锁解锁的过程,而是直接返回;

static MyCAS *GetInstance(){
    if(m_instance==NULL){//使用双重锁定(双重检查)这种方法提高效率
        std::unique_lock<std::mutex> mymutex(my_mutex);//自动加锁
        if(m_instance==NULL)
        {    
            m_instance=new MyCAS();
            static CgarHuiSHou cl;
        }
    }
    return m_instance;
}

四、std::call_once():C++11引入的函数,该函数的第二个参数是一个函数名a()

call_once()功能是保证函数a()只被调用一次;
call_once具备互斥量的功能,而且效率上比互斥量消耗的资源少;
call_once()需要与一个标记结合使用,这个标记std::once_flag,其实是一个结构;
call_once()就是通过这个标记来决定对应的函数a()是否执行,调用call_once()成功之后,这个标记就会被设置为已调用状态,后续再次调用call_once之后,检查once_flag是否是已调用状态,若已调用,则对应的函数a()就不会再执行了;

改写程序:假设两个线程同时执行到call_once,其中一个线程要等待另外一个线程执行完毕CreatInstance(),然后检查g_flag的状态来决定是否执行函数,此时g_flag的作用就相当于一个互斥量。

using namespace std;

std::mutex my_mutex;//创建一个互斥量
std::once_flag g_flag;//创建一个标记

class MyCAS//单例类创建
{
    static void CreatInstance(){//只能被调用一次的函数
        m_instance=new MyCAS();
        static CgarHuiSHou cl;
    }
private:
    MyCAS(){}//构造函数私有化,之后不能直接使用 MyCAS A; 这种方法创建对象

private:
    static MyCAS*m_instance;//静态成员变量,MyCAS类指针
public:
    static MyCAS *GetInstance(){
        std::call_once(g_flag,CreatInstance);//假设两个线程同时执行到这里,其中一个线程要等另外一个线程执行完毕CreatInstance(),此时g_flag的作用就相当于一个互斥量
        return m_instance;
    }

    class CgarHuiSHou//类中套类,用来释放对象
    {
    public:
        ~CgarHuiSHou()//类的析构函数中
        {
            if(MyCAS::m_instance){
                delete MyCAS::m_instance;
                MyCAS::m_instance=NULL;
            }
        }
    };

    void func(){
        cout<<"测试"<<endl;
    }

};
//类静态变量成员初始化
MyCAS *MyCAS::m_instance=NULL;
void *mythread(){
    cout<<"我的线程开始执行了"<<endl;
    MyCAS *p_a=MyCAS::GetInstance();
    cout<<"我的线程结束了"<<endl;
    return;
}

int main(){
    std::thread mytobj1(mythread);//这两个线程都以mythread为入口函数,
    std::thread mytobj2(mythread);//这是两个线程,即这里会有两条通路同时执行GetInstance()这个函数,
    mytobj1.join();
    mytobj2.join();

    return 0;
}

建议一般最好在主线程中就把单例对象创建了,避免在子线程中创建而导致出现问题;

MyCAS *p_a=MyCAS::GetInstance();//主线程中创建一个单例对象,返回该类对象的指针;
//然后再创建子线程,直接就可以在子线程中使用这个单例对象
std::thread mytobj1(mythread);//这两个线程都以mythread为入口函数,
std::thread mytobj2(mythread);

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

c++11并发与多线程【第六节】unique_lock详解

第六节 unique_lock详解

一、unique_lock取代lock_guard

unique_lock是个类模板,一般情况工作中使用lock_guard就足够了;
lock_guard取代了mutex的lock()和unlock();
unique_lock相比lock_guard更灵活,效率上差一点,内存占有多一点;

使用方法:

std::unique_lock<std::mutex> lockguard(my_mutex1);

二、unique_lock的第二个参数

2.1 std::adopt_lock

与lock_guard的第二个参数类似,表示这个互斥量已经被lock了,(当然在使用这个参数之前,必须要把互斥量lock,否则会报错);
std::adopt_lock标记得效果就是“假设调用方线程已经拥有了这个互斥量的所有权,因为已经lock()成功了”;
然后通知unique_lock或者lock_guard不需要在构造函数中lock这个互斥量了;

my_mutex1.lock();//先lock之后才能用std::adopt_lock
std::unique_lock<std::mutex> lockguard(my_mutex1,std::adopt_lock);

2.2 std::try_to_lock

体现unique_lock灵活性案例:
线程一中:

std::unique_lock<std::mutex> lockguard(my_mutex1);
//做个延时,lock成功之后休息20秒
std::chrono::milliseconds dura(20000);//单位毫秒
std::this_thread::sleep_for(dura);//休息20秒

线程二中:

my_mutex1.lock();//先lock之后才能用std::adopt_lock
std::unique_lock<std::mutex> lockguard(my_mutex1,std::adopt_lock);

则当线程一lock成功之后,线程二由于无法lock会阻塞在这,直到线程一unlock,此时线程二由于阻塞无法执行任何操作;

使用std::try_to_lock之后,尝试使用mutex的lock()去锁定这互斥量mutex,但是如果没有锁定成功,就会立即返回,并不会一直阻塞在这个地方;
使用这个std::try_to_lock的前提是互斥量不能被自己先lock()了;

//
            std::unique_lock<std::mutex> lockguard(my_mutex1,std::try_to_lock);//
            //判断是否lock()成功
            if(lockguard.owns_lock()){
                msgRecvQueue.push_back(i);//把命令放入队列当中
                //其他处理代码
                cout<<"inMsgRecvQueue,lock成功了"<<endl;
            }
            else{
                //没拿到锁
                cout<<"inMsgRecvQueue,没有lock成功 :"<<i<<endl;
            }

从上面代码可以看出,即使没有lock成功,整个线程也不会一直阻塞,而是可以去干别的事情;

2.3 std::defer_lock

使用std::defer_lock使用前提也是不能自己先lock();
defer_lock的意义是:初始化一个没有加锁的mutex,也就是说这个unique_lock对象不会在构造函数中自动给mutex加锁;

三. unique_lock的成员函数

3.1 lock()成员函数和unlock()成员函数,搭配defer_lock使用

std::unique_lock<std::mutex> lockguard(my_mutex1,std::defer_lock);//没有lock的my_mutex1
lockguard.lock();//调用unique_lock的对象来lock(),不使用mutex的lock

使用情况:

 //把收到的消息(玩家命令)放入到一个队列的线程入口函数
 void inMsgRecvQueue(){
     for(int i=0;i<10000;i++){//用数字模拟玩家发送来的命令
         cout<<"inMsgRecvQueue()执行,插入一个元素 "<<i<<endl;
         //
         std::unique_lock<std::mutex> lockguard(my_mutex1,std::defer_lock);//没有lock的my_mutex1
         lockguard.lock();//调用unique_lock的对象来lock(),不使用mutex的lock
         //处理一些共享代码
         lockguard.unlock();//相当于临时性解锁去执行一些非共享代码

         //处理非共享代码

         lockguard.lock();
         
         msgRecvQueue.push_back(i);//把命令放入队列当中
         //其他处理代码
         cout<<"inMsgRecvQueue,lock成功了"<<endl;

lockguard.unlock();///倘若不使用也可以,unique_lock的析构函数会作用域结束后会自动解锁
     }
 }

3.2 try_lock()成员函数,搭配defer_lock使用

尝试给互斥量加锁,如果不能锁,返回false,如果锁住了,返回true,这个函数不阻塞;

//把收到的消息(玩家命令)放入到一个队列的线程入口函数
void inMsgRecvQueue(){
    for(int i=0;i<10000;i++){//用数字模拟玩家发送来的命令
        
        //
        std::unique_lock<std::mutex> lockguard(my_mutex1,std::defer_lock);//没有lock的my_mutex1
        if(lockguard.try_lock()==true)//返回true表示拿到锁了
        {
            cout<<"inMsgRecvQueue()执行,插入一个元素 "<<i<<endl;
            msgRecvQueue.push_back(i);//把命令放入队列当中
            //其他处理代码
            cout<<"inMsgRecvQueue,lock成功了"<<endl;
        }
        else{
            cout<<"没有锁成功  "<<i<<endl;
        }
    }
}

3.3 release() 成员函数

返回它所管理的mutex对象指针,并释放所有权;也就是说unique_lock()和mutex不再有关系;
注意区分release和unlock;
下面这种写法相当于把mutex的对象my_mutex1与unique_lock绑定在一起,然后使用unique_lock来管理mutex对象;

std::unique_lock<std::mutex> lockguard(my_mutex1,std::defer_lock);

release()放弃了unique_lock与mutex对象的关系;
如果,mutex对象已经被lock了,release()之后要用户要负责对mutex进行unlock();

std::unique_lock<std::mutex> lockguard(my_mutex1);//构造函数中lock()
std::mutex *ptx=lockguard.release();//现在需要自己解锁,由用户接管mutex
//****共享数据操作
ptx->unlock();

release()返回的是std::mutex的指针;

结论:

为什么有时候需要成员函数unlock()?因为lock主的代码越少,执行越快,整个程序的运行效率越高。
也有人把锁头锁住的代码的多少称为锁的粒度,粒度一般用粗细来形容;
1.锁住的代码多,粒度粗,执行效率低;
2锁住的代码少,粒度细,执行效率高;

要选择合适粒度的代码进行保护,粒度太细,容易漏掉共享数据的保护,太粗,效率低;

四. unique_lock所有权的传递

一般情况下,unique_lock需要对一个mutex对象进行管理;一个mutex对象的所有权只能被一个unique_lock所拥有;

std::unique_lock<std::mutex> test(my_mutex1);

此时test拥有my_mutex1的所有权;
test可以把自己对my_mutex1的的所有权转移给其他的unique_lock对象;
unique_lock对象这个mutex的所有权可以使用std::move(unique_lock对象)来进行转移;

std::unique_lock<std::mutex> lockguard(my_mutex1);//构造函数中lock()
std::unique_lock<std::mutex> lockguard2(std::move(lockguard));//移动语义,相当于lockguard2与my_mutex1绑定在一起,拥有了my_mutex1的所有权,此时lockguard指向空

使用unique_lock作为函数返回值传递mutex对象的所有权:

std::unique_lock<std::mutex> rtn_unique_lock()
{
    std::unique_lock<std::mutex> tempguard(my_mutex1);
    return tempguard;//返回一个局部的unique_lock对象,返回这种局部对象会导致系统生成临时的unique_lock对象,并调用unique_lock的移动构造函数
}
std::unique_lock<std::mutex> sguard=rtn_unique_lock();//此时sguard可以接受到my_mutex1的所有权;

所有权传递的方法:
1.std::move()

2.函数return

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

c++11并发与多线程【第五节】互斥量概念,用法,死锁演示及解决

第五节 互斥量概念,用法,死锁演示及解决

关于进程和线程管理,这里讲的只限于这里的案例,更多的关于进程线程管理,死锁管理,同步互斥关系,强烈建议先去学习《操作系统》。

一、互斥量(mutex)的概念

互斥:当一个进程或线程使用共享数据时,另一个线程或进程必须等待,当占用共享数据的线程或进程退出后,另一个线程或进程才允许去访问该共享数据。实现方式:操作时,某个线程用代码把共享数据锁住,操作数据,解锁;其他想操作数据的线程必须等待解锁,然后锁住,操作,解锁。

互斥量是个类对象,理解成一把锁,多个线程会尝试使用这个对象的lock()成员函数来加锁这把锁头,但是只有一个线程能锁定成功,如果没锁成功,那么该线程便会阻塞在这个地方;

二、互斥量(mutex)的用法

互斥量包含在一个头文件中,使用是需要包含该头文件#include
新建一个互斥量:std::mutex my_mutex;

2.1 lock(),unlock()

步骤:先lock(),操作数据,然后unlock();

lock()与unlock()必须成对使用。

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

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

    bool MsgProcess(int &command){

        my_mutex.lock();//加锁
        if(!msgRecvQueue.empty()){
            command=msgRecvQueue.front();//返回第一个元素
            msgRecvQueue.pop_front();//取出后移除该元素
            my_mutex.unlock();//解锁
            //然后处理数据
            return true;
        }
        my_mutex.unlock();//解锁

        return false;
    }
    //从消息队列list中读取玩家命令的线程入口函数
    void outMsgRecvQueue(){
        int command=0;
        for(int i=0;i<10000;i++){
            //
            bool result=MsgProcess(command);
            if(result){
                cout<<"outMsgRecvQueue执行,取出一个元素 "<<command<<endl;
                //然后对数据进行处理
            }
            else
                cout<<"outMsgRecvQueue执行,但是list已经空了 : "<<i<<endl;
        }
        cout<<"end "<<endl;
    }

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

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

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

两个线程一次只能有一个能lock()成功,具体是那个lock()成功,由操作系统决定。

lock()与unlock()必须成对出现,为了防止lock后忘记unlock,C++引入了std::lock_guard的类模板,自动进行unlock;

2.2 std::lock_guard类模板:直接取代lock()和unlock()

一旦使用了lock_guard之后,便不能再使用lock和unlock;
使用方法:

std::lock_guard<std::mutex> lockguard(my_mutex);//lock_guard的类构造函数里执行了mutex::lock(),lock_guard的析构函数里执行了mutex::unlock()

lock_guard的类构造函数里执行了mutex::lock(),lock_guard的析构函数里执行了mutex::unlock();
例如:

bool MsgProcess(int &command){
    std::lock_guard<std::mutex> lockguard(my_mutex);//lock_guard的类构造函数里执行了mutex::lock(),lock_guard的析构函数里执行了mutex::unlock()
    
    //my_mutex.lock();//加锁
    if(!msgRecvQueue.empty()){
        command=msgRecvQueue.front();//返回第一个元素
        msgRecvQueue.pop_front();//取出后移除该元素
        //my_mutex.unlock();//解锁
        //然后处理数据
        return true;
    }
    //my_mutex.unlock();//解锁
    return false;
}

注意:lock_guard()的unlock发生在作用域结束之前,对与其作用域要特别注意。对共享数据的访问一定要限制在lock_guard的作用域内。

三、死锁

张三:站在北京 等李四
李四:站在深圳 等张三

3.1 死锁概念:两个或以上的进程或线程,在本身都占有资源的情况下,请求其他线程或进程占有的资源,形成了一个环,导致每个线程或进程都无法进行执行的情况。

死锁的详细概念与形成条件,可以看《操作系统》;
死锁的必要条件:

  1. 互斥,所分配的资源一次只能为一个进程或线程所占有;
  2. 非剥夺,进程或线程所获得的资源在为使用完毕之前,不能被其他进程或线程强行夺走;
  3. 请求并保持,进程或线程已经至少拥有了一个资源,同时又请求其他进程或线程所占有的资源;
  4. 循环等待,互相等待对方所占有资源形成了一个循环等待链,每个进程或线程已占有的资源被链中的下一个进程或线程所请求;

C++中,例子:
两个线程A,B:
线程A执行的时候,锁住了金锁,把金锁锁住成功了,然后去lock银锁;突然出现了上下文切换,线程B开始执行了,线程B先锁银锁,成功了,然后线程B去lock金锁;此时死锁就发生了。此时线程A因为锁不了银锁而阻塞,线程B因为锁不了金锁而阻塞。

死锁的情况:
线程1:先锁住了互斥量my_mutex1,再锁住my_mutex2

my_mutex1.lock();//加锁
//****如果中间有其他代码
my_mutex2.lock();
msgRecvQueue.push_back(i);//把命令放入队列当中
my_mutex1.unlock();//解锁
my_mutex2.unlock();

线程2:先锁住了互斥量my_mutex2,再锁住my_mutex1

my_mutex2.lock();//加锁
my_mutex1.lock();
if(!msgRecvQueue.empty()){
    command=msgRecvQueue.front();//返回第一个元素
    msgRecvQueue.pop_front();//取出后移除该元素
    my_mutex1.unlock();//解锁
    my_mutex2.unlock();
    //然后处理数据
    return true;
}
my_mutex1.unlock();//解锁
my_mutex2.unlock();

此时便会发生死锁。

3.2 死锁的解决办法

这里只从当前这个代码的角度来解决死锁,其他更详细的的死锁解决方法看《操作系统》

这里只需要保证线程一和线程二,加锁的顺序一致,就可以避免死锁。只要保证两个互斥量上锁的顺序一致就不会死锁。

3.3 std::lock()函数模板:处理多个互斥量时才出场

功能:一次锁住两个或两个以上的互斥量(至少两个,多了不限),但是一般使用情况比较少;
它不存在这种因为在多个线程中,因为锁的顺序导致死锁的风险问题。
std::lock(),如果要锁住的互斥量中有一个没锁住,就会阻塞,并且会把已经能锁住的互斥量解锁释放,等所有的互斥量都可以锁住之后,它才能往下走。
要么两个互斥量都能锁住,要么都没锁住

使用方法:

std::lock(my_mutex1,my_mutex2);//参数my_mutex1和my_mutex2顺序无所谓
//相关代码
my_mutex1.unlock();//解锁
my_mutex2.unlock();

3.4 std::lock_guard的std::adopt_lock参数

实际上类似于std::lock()与lock_guard()相结合的方法。

std::lock(my_mutex1,my_mutex2);
std::lock_guard<std::mutex> lockguard(my_mutex1,std::adopt_lock);//
std::lock_guard<std::mutex> lockguard(my_mutex2,std::adopt_lock);//
//相关代码
//my_mutex1.unlock();//解锁
//my_mutex2.unlock();
//

此时可以不用unlock();

std::adopt_lock参数是个结构体对象,起一个标记作用:表示这互斥量已经lock()过了,不需要在std::lock_guardstd::mutex的构造函数里对mutex对象进行lock了。

关于进程和线程管理,这里讲的只限于这里的案例,更多的关于进程线程管理,死锁管理,同步互斥关系,强烈建议先去学习《操作系统》。

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

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

请我喝杯咖啡吧~

支付宝
微信