为了实现在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
除了上面概念中所提到的,还有:
进程获取时间,线程占用内存
在一个应用程序(进程)中同时执行多个小的部分(线程),这就是多线程。多个线程虽然共享一样的数据,但是却执行不同的任务。
并发是指在同一个时间里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 并发编程 主要包含了与条件变量相关的类和函数
是C++标准程序库中的一个头文件,定义了C++11标准中一些互斥访问的类与方法。
其中std::mutex表示普通互斥锁,可以与std::unique_lock配合使用,把std::mutex放到unique_lock中时,mutex会自动上锁,unique_lock析构时,同时把mutex解锁。因此std::mutex可以保护同时被多个线程访问的共享数据,并且它独占对象所有权,不支持对对象递归上锁。
可以这样理解:各个线程在对共享资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。(下图来自网络)
常用的成员函数有:
一个简单的例子:
std::mutex mtx;//创建一个互斥锁
static void print_(int n,char c)
{
mtx.lock() //申请访问资源,对该内存资源上锁
for(int i=0;i
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。
其常用的成员函数为:
例如这样使用:
std::mutex mtx//定义一个互斥锁
void print_thread_id(int id){
std::unique_lock lck(mtx);
lck.lock();
std::cout<<"thread #"<
条件变量的引入是为了作为并发程序设计中的一种控制结构:当多个线程访问同一共享资源时,不但需要用互斥锁实现独享访问以避免并发错误(竞争危害),在获得互斥锁进入临界区后还需要检验特定条件是否成立:
条件变量std::condition_variable用于多线程之间的通信,它可以阻塞一个或同时阻塞多个线程。std::condition_variable需要与std::unique_lock配合使用。
常用成员函数:
(1)构造函数:仅支持默认构造函数。
(2)wait():当前线程调用wait()后将被阻塞,直到另外某个线程调用notify_ *唤醒当前线程。当线程被阻塞时,该函数会自动调用std::mutex的unlock()释放锁,使得其它被阻塞在锁竞争上的线程得以继续执行。一旦当前线程获得通知(notify,通常是另外某个线程调用notify_ *唤醒当前线程),wait()函数自动调用std::mutex的lock()。wait分为无条件被阻塞和带条件的被阻塞两种:
(3)notify_all: 唤醒所有的wait线程,如果当前没有等待线程,则该函数什么也不做。
(4)notify_one:唤醒某个wait线程,如果当前没有等待线程,则该函数什么也不做;如果同时存在多个等待线程,则唤醒某个线程是不确定的(unspecified)。
简单的说就是,当std::condition_variable对象的某个wait函数被调用的时候,它使用std::unique_lock(通过std::mutex)来锁住当前线程。当前线程会一直被阻塞,直到另外一个线程在相同的std::condition_variable对象上调用了notification函数来唤醒当前线程。
是C++标准程序库中的一个头文件,定义了C++11标准中一些表示线程、并发控制时进行原子操作的类与方法,主要声明了两大类原子对象:std::atomic和std::atomic_flag。
原子操作的主要特点是原子对象的并发访问不存在数据竞争,利用原子对象可实现数据结构的无锁设计。在多线程并发执行时,原子操作是线程不会被打断的执行片段。
(1)atomic_flag类
是一种简单的原子bool类型
只支持两种操作:test_and_set(flag=true)和clear(flag=false)。
跟std::atomic的其它所有特化类不同,它是锁无关的。
结合std::atomic_flag::test_and_set()和std::atomic_flag::clear(),std::atomic_flag对象可以当作一个简单的自旋锁(spin lock)使用。
atomic_flag只有默认构造函数,禁用拷贝构造函数,移动构造函数实际上也禁用。
如果在初始化时没有明确使用宏ATOMIC_FLAG_INIT初始化,那么新创建的std::atomic_flag对象的状态是未指定的(unspecified),既没有被set也没有被clear;如果使用该宏初始化,该std::atomic_flag对象在创建时处于clear状态。
(2)std::atomic类
std::atomic 常用的成员函数:
多线程模型共享同一进程资源,通过多线程可以极大的提高代码的效率,完成单一线程无法完成的任务。
几个需要记住的点: C++中的线程是一个类
,因此可以像操作类一样进行操作; C++中的线程也是一类资源
;
线程的默认构造函数为std::thread thread_object(callable)
一个可调用对象可以是以下三个中的任何一个:
定义 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()
共享数据的管理 和 线程间的通信 是多线程编程的两大核心
参数为引用类型时的处理
注: 线程传递参数默认都是
值传递
, 即使参数的类型是引用,也会被转化 如果在线程中使用引用来更新对象时,就需要注意了。默认的是将对象拷贝到线程空间,其引用的是拷贝的线程空间的对象,而不是初始希望改变的对象. 解决方案:使用std::ref()
thread t(func, std::ref(data))
在创建和启动线程传入线程函数时,其需要采用引用
方式的参数用std::ref()
进行修饰,如此,在t线程中对data的修改会反馈到当前线程中。
// 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函数
线程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)来让函数一直运行