C++多线程编程

C++多线程编程

为了实现在ORB基础上添加一个线程,学习多线程编程的相关知识

参考资料

[菜鸟教程]https://www.runoob.com/cplusplus/cpp-multithreading.html)

[Cmakelist万能模板]https://blog.csdn.net/qq_16401691/article/details/125644353

多线程(c++/python)https://zhuanlan.zhihu.com/p/77965207

如何实现C++中的多线程编程https://mp.weixin.qq.com/s/61iCytHD8CTMMPxWxpXhOQ

基本概念

1.1 进程

  • 简单理解为程序的一次执行,例如在桌面打开一个应用程序就开启了一个进程。进程通常由程序、数据、进程控制块(PCB)构成。
  • 传统进程认为:进程可以获取操作系统分配的资源,如内存等;可以参与操作系统的调度,参与CPU的竞争,得到分配的时间片,获得CPU运行。
  • 进程在创建、撤销和切换中,系统必须为之付出较大的时空开销,因此系统中开启的进程数不宜过多。也就是系统开启太多程序,内存卡爆了。

1.2 线程

  • 线程是进程中的一个实体,是被系统独立分配和调度的基本单位。或者说,线程是CPU可执行调度的最小单位,进程本身并不能获取CPU时间,只有它的线程才可以。(我理解就是一个从主系统分工出来的子系统,子系统上可以跑不同的进程)
  • 引入线程后,不同于传统进程:线程参与操作系统的调度,参与CPU的竞争,得到分配的时间片,获得处理机(CPU)运行;而进程负责获取操作系统分配的资源,如内存。
  • 线程基本上不拥有资源,只拥有一点运行中必不可少的资源,它可与同属一个进程的其他线程共享进程所拥有的全部资源。同一个进程中的多个线程可以并发执行

1.3 线程与线程的区别联系

除了上面概念中所提到的,还有:

  • 线程分为用户级线程和内核支持线程两类:用户级线程不依赖于内核,该类线程的创建、撤销和切换都不利用系统调用来实现;内核支持线程依赖于内核,即无论是在用户进程中的线程,还是在系统中的线程,其创建、撤销和切换都利用系统调用来实现。
  • 系统进程和用户进程在进行切换时都要依赖于内核中的进程调度。因此,无论是什么进程都是与内核有关的,是在内核支持下进程切换的。
  • 进程要独立的占用系统资源(如内存),而同一进程的线程之间是共享资源的。进程本身并不能获取CPU时间,只有它的线程才可以。

进程获取时间,线程占用内存

在一个应用程序(进程)中同时执行多个小的部分(线程),这就是多线程。多个线程虽然共享一样的数据,但是却执行不同的任务。

1.4 并发

并发是指在同一个时间里CPU同时执行两条或多条命令。单核CPU和C++11以前实现的并发一般是伪并发。但随着多核CPU的普及,C++11开始支持真正意义上的并发。

C++11可以通过多线程实现并发,这是一种比较底层、传统的实现方式。C++11引入了5个头文件来支持多线程编程:////

#include    // C++11 原子操作,限制并发程序对共享数据的使用,避免数据竞争
#include    // 该头文件主要声明了std::thread类,另外std::this_thread命名空间也在该头文件中
#include     // C++11 互斥量Mutex。在多线程环境中,有多个线程竞争同一个公共资源,就很容易引发线程安全的问题
#include   // C++11 并发编程 主要包含了与条件变量相关的类和函数

2 std::mutex 互斥访问

是C++标准程序库中的一个头文件,定义了C++11标准中一些互斥访问的类与方法。

其中std::mutex表示普通互斥锁,可以与std::unique_lock配合使用,把std::mutex放到unique_lock中时,mutex会自动上锁,unique_lock析构时,同时把mutex解锁。因此std::mutex可以保护同时被多个线程访问的共享数据,并且它独占对象所有权,不支持对对象递归上锁。

可以这样理解:各个线程在对共享资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。(下图来自网络)

C++多线程编程_第1张图片

常用的成员函数有:

  1. 构造函数:std::mutex不支持copy和move操作,最初的mutex对象处于unlocked状态。
  2. lock函数:互斥锁被锁定。如果线程申请该互斥锁,但未能获得该互斥锁,则申请调用的线程将阻塞(block)在该互斥锁上;如果成功获得该互诉锁,则该线程一直拥有互斥锁直到调用unlock解锁;如果该互斥锁已经被当前调用线程锁住,则产生死锁(deadlock)。
  3. unlock函数:互斥锁解锁,释放调用线程对该互斥锁的所有权。释放内存资源

一个简单的例子:

std::mutex mtx;//创建一个互斥锁
static void print_(int n,char c)
{
    mtx.lock() //申请访问资源,对该内存资源上锁
     for(int i=0;i

3 std::unique_lock 锁管理模板类

std::unique_lock为锁管理模板类,是对通用mutex的封装。

std::unique_lock对象以独占所有权的方式(unique owership)管理mutex对象的上锁和解锁操作,即在unique_lock对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而unique_lock的生命周期结束之后,它所管理的锁对象会被解锁。因此用unique_lock管理互斥对象,可以作为函数的返回值,也可以放到STL的容器中。

在使用条件变量std::condition_variable时需要使用std::unique_lock而不能使用std::lock_guard。

其常用的成员函数为:

  1. unique_lock构造函数:禁止拷贝构造,允许移动构造;
  2. lock函数:调用所管理的mutex对象的lock函数;
  3. unlock函数:调用所管理的mutex对象的unlock函数;

例如这样使用:

std::mutex mtx//定义一个互斥锁
void print_thread_id(int id){
    std::unique_lock lck(mtx);
    lck.lock();
    std::cout<<"thread #"<

4 std::condition_variable 条件变量

是C++标准程序库中的一个头文件,定义了C++11标准中的一些用于并发编程时表示条件变量的类与方法等。

条件变量的引入是为了作为并发程序设计中的一种控制结构:当多个线程访问同一共享资源时,不但需要用互斥锁实现独享访问以避免并发错误(竞争危害),在获得互斥锁进入临界区后还需要检验特定条件是否成立

  1. 若不满足该条件,拥有互斥锁的线程应该释放该互斥锁,使用unique_lock函数把自身阻塞(block)并挂到条件变量的线程队列中
  2. 若满足该条件,拥有互斥锁的线程在临界区内访问共享资源,在退出临界区时通知(notify)在条件变量的线程队列中处于阻塞状态的线程,被通知的线程必须重新申请对该互斥锁加锁。

条件变量std::condition_variable用于多线程之间的通信,它可以阻塞一个或同时阻塞多个线程。std::condition_variable需要与std::unique_lock配合使用。

常用成员函数:

(1)构造函数:仅支持默认构造函数。

(2)wait():当前线程调用wait()后将被阻塞,直到另外某个线程调用notify_ *唤醒当前线程。当线程被阻塞时,该函数会自动调用std::mutex的unlock()释放锁,使得其它被阻塞在锁竞争上的线程得以继续执行。一旦当前线程获得通知(notify,通常是另外某个线程调用notify_ *唤醒当前线程),wait()函数自动调用std::mutex的lock()。wait分为无条件被阻塞和带条件的被阻塞两种:

  1. 无条件被阻塞:调用该函数之前,当前线程应该已经对unique_lock lck完成了加锁。所有使用同一个条件变量的线程必须在wait函数中使用同一个unique_lock。该wait函数内部会自动调用lck.unlock()对互斥锁解锁,使得其他被阻塞在互斥锁上的线程恢复执行。使用本函数被阻塞的当前线程在获得通知(notified,通过别的线程调用 notify_*系列的函数)而被唤醒后,wait()函数恢复执行并自动调用lck.lock()对互斥锁加锁。
  2. 带条件的被阻塞:wait函数设置了谓词(Predicate),只有当pred条件为false时调用该wait函数才会阻塞当前线程,并且在收到其它线程的通知后只有当pred为true时才会被解除阻塞。因此,等效于while (!pred()) wait(lck).

(3)notify_all: 唤醒所有的wait线程,如果当前没有等待线程,则该函数什么也不做。

(4)notify_one:唤醒某个wait线程,如果当前没有等待线程,则该函数什么也不做;如果同时存在多个等待线程,则唤醒某个线程是不确定的(unspecified)。

简单的说就是,当std::condition_variable对象的某个wait函数被调用的时候,它使用std::unique_lock(通过std::mutex)来锁住当前线程。当前线程会一直被阻塞,直到另外一个线程在相同的std::condition_variable对象上调用了notification函数来唤醒当前线程。

5 std::atomic 原子操作

是C++标准程序库中的一个头文件,定义了C++11标准中一些表示线程、并发控制时进行原子操作的类与方法,主要声明了两大类原子对象:std::atomic和std::atomic_flag。

原子操作的主要特点是原子对象的并发访问不存在数据竞争,利用原子对象可实现数据结构的无锁设计。在多线程并发执行时,原子操作是线程不会被打断的执行片段。

(1)atomic_flag类

  1. 是一种简单的原子bool类型

    只支持两种操作:test_and_set(flag=true)和clear(flag=false)。

  2. 跟std::atomic的其它所有特化类不同,它是锁无关的。

  3. 结合std::atomic_flag::test_and_set()和std::atomic_flag::clear(),std::atomic_flag对象可以当作一个简单的自旋锁(spin lock)使用。

  4. atomic_flag只有默认构造函数,禁用拷贝构造函数,移动构造函数实际上也禁用。

  5. 如果在初始化时没有明确使用宏ATOMIC_FLAG_INIT初始化,那么新创建的std::atomic_flag对象的状态是未指定的(unspecified),既没有被set也没有被clear;如果使用该宏初始化,该std::atomic_flag对象在创建时处于clear状态。

(2)std::atomic类

  1. std::atomic提供了针对bool类型、整形(integral)和指针类型的特化实现。每个std::atomic模板的实例化和完全特化定义一个原子类型。
  2. 若一个线程写入原子对象,同时另一个线程从它读取,则行为良好定义。
  3. 原子对象的访问可以按std::memory_order所指定建立线程间同步,并排序非原子的内存访问。
  4. std::atomic可以以任何可平凡复制(Trivially Copyable)的类型T实例化。
  5. std::atomic既不可复制亦不可移动。
  6. ATOMIC_VAR_INIT(val):可以由构造函数直接执行此宏初始化std::atomic对象。

std::atomic 常用的成员函数:

  1. std::atomic::store(val) 函数将参数 val 复制给原子对象所封装的值。
  2. std::atomic::load() 读取被原子对象封装的值。
  3. std::atomic::exchange(val) 读取并修改被封装的值,exchange 会将 val 指定的值替换掉之前该原子对象封装的值,并返回之前该原子对象封装的值,整个过程是原子的.
  4. atomic() 默认构造函数,由默认构造函数创建的 std::atomic 对象处于未初始化(uninitialized)状态,对处于未初始化(uninitialized)状态 std::atomic对象可以由 atomic_init 函数进行初始化。
  5. atomic (T val) 初始化构造函数,由类型 T初始化一个 std::atomic对象。
  6. atomic (const atomic&) 拷贝构造函数被禁用。

多线程

多线程模型共享同一进程资源,通过多线程可以极大的提高代码的效率,完成单一线程无法完成的任务。

​ 几个需要记住的点: C++中的线程是一个,因此可以像操作类一样进行操作; C++中的线程也是一类资源

线程的默认构造函数为std::thread thread_object(callable)

一个可调用对象可以是以下三个中的任何一个:

  • 函数指针
  • 函数对象
  • lambda 表达式

定义 callable 后,将其传递给 std::thread 构造函数 thread_object

代码示例:

// 演示多线程的CPP程序
// 使用三个不同的可调用对象
#include 
#include 
using namespace std;
 
// 一个虚拟函数
void foo(int Z)
{
    for (int i = 0; i < Z; i++) {
        cout << "线程使用函数指针作为可调用参数\n";
    }
}
 
// 可调用对象
class thread_obj {
public:
    void operator()(int x)
    {
        for (int i = 0; i < x; i++)
            cout << "线程使用函数对象作为可调用参数\n";
    }
};
 
int main()
{
    cout << "线程 1 、2 、3 "
         "独立运行" << endl;
 
    // 函数指针
    thread th1(foo, 3);
 
    // 函数对象
    thread th2(thread_obj(), 3);
 
    // 定义 Lambda 表达式
    auto f = [](int x) {
        for (int i = 0; i < x; i++)
            cout << "线程使用 lambda 表达式作为可调用参数\n";
    };
 
    // 线程通过使用 lambda 表达式作为可调用的参数
    thread th3(f, 3);
 
    // 等待线程完成
    // 等待线程 t1 完成
    th1.join();
 
    // 等待线程 t2 完成
    th2.join();
 
    // 等待线程 t3 完成
    th3.join();
 
    return 0;
}

运行结果:

/home/run/桌面/SLAM/TemCode/cmake-build-debug/TemCode
线程 1 、2 、3 独立运行
线程使用 lambda 表达式作为可调用参数
线程使用 lambda 表达式作为可调用参数
线程使用 lambda 表达式作为可调用参数
线程使用函数对象作为可调用参数
线程使用函数对象作为可调用参数
线程使用函数对象作为可调用参数
线程使用函数指针作为可调用参数
线程使用函数指针作为可调用参数
线程使用函数指针作为可调用参数

进程已结束,退出代码0

由于pthread 库不是 Linux 系统默认的库,连接时需要使用静态库 libpthread.a,所以在使用pthread_create()创建线程时,在编译中要加 -lpthread参数。如果用Cmakelist编译加一行链接库link_libraries(pthread)

以上是多线程下的HelloWorld!,从上我们可以看出C++多线程编程的基本步骤:

thread_task是子线程,thread t(thread_task)意思是启动这个子线程,join()会使主线程会被阻塞,直到子线程执行完毕,然后才退程序。

创建线程函数 -> 实例一个线程 -> 运行

线程管理初步

包括线程函数,启动线程,结束线程,线程传参

join()和detach()

C++多线程编程_第2张图片

线程传参

共享数据的管理线程间的通信 是多线程编程的两大核心

参数为引用类型时的处理

注: 线程传递参数默认都是值传递, 即使参数的类型是引用,也会被转化   如果在线程中使用引用来更新对象时,就需要注意了。默认的是将对象拷贝到线程空间,其引用的是拷贝的线程空间的对象,而不是初始希望改变的对象. 解决方案:使用std::ref()

thread t(func, std::ref(data))

在创建和启动线程传入线程函数时,其需要采用引用方式的参数用std::ref()进行修饰,如此,在t线程中对data的修改会反馈到当前线程中。

多线程实例 DS-SLAM中语义线程部分

system.cc中创建线程,以及传入参数

 // Initialize the Local Mapping thread and launch
    mpLocalMapper = new LocalMapping(mpMap, mSensor==MONOCULAR);
    mptLocalMapping = new thread(&ORB_SLAM2::LocalMapping::Run,mpLocalMapper);

    // Initialize the Loop Closing thread and launch
    mpLoopCloser = new LoopClosing(mpMap, mpKeyFrameDatabase, mpVocabulary, mSensor!=MONOCULAR);
    mptLoopClosing = new thread(&ORB_SLAM2::LoopClosing::Run, mpLoopCloser);

    // Semantic segmentation thread
    mpSegment =new Segment( pascal_prototxt, pascal_caffemodel, pascal_png);
    mptSegment =new thread(&ORB_SLAM2::Segment::Run,mpSegment);
    mpSegment->SetTracker(mpTracker);
    // Set pointers between threads
    mpTracker->SetLocalMapper(mpLocalMapper);
    mpTracker->SetLoopClosing(mpLoopCloser);
    mpTracker->SetSegment(mpSegment);
    mpLocalMapper->SetTracker(mpTracker);
    mpLocalMapper->SetLoopCloser(mpLoopCloser);

    mpLoopCloser->SetTracker(mpTracker);
    mpLoopCloser->SetLocalMapper(mpLocalMapper);
    
    //Initialize the Viewer thread and launch
    mpViewer = pViewer;
	if (mpViewer != NULL)
	{
        mpViewer->Register(this);
        mptViewer = new thread(&Viewer::Run, mpViewer);
        mpTracker->SetViewer(mpViewer);
	}

mptSegment =new thread(&ORB_SLAM2::Segment::Run,mpSegment);

传递mpSegment对象给ORB_SLAM2::Segment::Run函数

Segment.cc中具体函数实现

线程callback()函数

void Segment::Run()
{
    //分类器初始化
    classifier=new Classifier(model_file, trained_file);
    cout << "Load model ..."<<endl;
    //保持线程一直运行
    while(1)
    {

        usleep(1);
        if(!isNewImgArrived())//
        continue;

        cout << "Wait for new RGB img time =" << endl;
        if(mSkipIndex==SKIP_NUMBER)//相隔SKIP_NUMBER帧分割一次
        {
            std::chrono::steady_clock::time_point t3 = std::chrono::steady_clock::now();
            // Recognise by Semantin segmentation
            mImgSegment=classifier->Predict(mImg, label_colours);

            mImgSegment_color = mImgSegment.clone();
            cv::cvtColor(mImgSegment,mImgSegment_color, CV_GRAY2BGR);

            LUT(mImgSegment_color, label_colours, mImgSegment_color_final);
            cv::resize(mImgSegment, mImgSegment, cv::Size(Camera::width,Camera::height) );
            cv::resize(mImgSegment_color_final, mImgSegment_color_final, cv::Size(Camera::width,Camera::height) );

            std::chrono::steady_clock::time_point t4 = std::chrono::steady_clock::now();
            mSegmentTime+=std::chrono::duration_cast<std::chrono::duration<double> >(t4 - t3).count();
            mSkipIndex=0;
            imgIndex++;
        }
        mSkipIndex++;
        ProduceImgSegment();
        if(CheckFinish())//线程停止信号
        {
            break;
        }
    }
}

改变变量所需要加锁的其他函数

下面这些函数都是只调用一次的函数,因此只有加锁没有解锁,在函数调用结束会自动解锁。

判断是否需要语义分割:

bool Segment::isNewImgArrived()
{
    unique_lock<mutex> lock(mMutexGetNewImg);
    //这里有些疑惑
    //是对mbNewImgFlag所在内存区域加锁只能当前线程获取
    //还是对mptSegment内的所以相关内存都加锁不让别的线程调用?
    if(mbNewImgFlag)
    {
        mbNewImgFlag=false;
        return true;
    }
    else
    return false;
}

判断线程是否需要结束

bool Segment::CheckFinish()
{
    unique_lock<mutex> lock(mMutexFinish);
    return mbFinishRequested;
}

设置结束

void Segment::RequestFinish()
{
    unique_lock<mutex> lock(mMutexFinish);
    mbFinishRequested=true;
}

处理图像分割

void Segment::ProduceImgSegment()
{
    std::unique_lock <std::mutex> lock(mMutexNewImgSegment);
    //前一帧更新为当前帧分割结果,当前帧里保存前一阵的结果,如果需要分割就更新mImgSegment
    mImgTemp=mImgSegmentLatest;
    mImgSegmentLatest=mImgSegment;
    mImgSegment=mImgTemp;
    mpTracker->mbNewSegImgFlag=true;
}

ORB-SLAM2和DS-SLAM都是通过初始化一个线程对象后,在线程调用的函数中写入while(1)来让函数一直运行

你可能感兴趣的:(C++编程练习,c++)