C++笔记之多线程编程1

本文介绍了C++11中的多线程编程,包括多线程原理、同步和互斥、线程同步方法(如临界区、事件、互斥量和信号量)以及线程创建的三种方式。通过示例代码展示了如何使用C++11的thread库创建线程,以及如何通过线程同步避免资源竞争。此外,还讨论了线程所有权的转移,强调了join和detach的使用场景。
摘要由CSDN通过智能技术生成

参考:C++ 多线程编程

C++多线程的三种创建方式 - 云+社区 - 腾讯云 (tencent.com)

一、多线程原理

线程是进程中的一个实体,是被系统独立分配和调度的基本单位。也就是说线程是CPU可执行调度的最小单位

注意:线程作为调度和分配的基本单位,进程作为独立分配资源的单位。

线程基本不拥有资源,只拥有一点运行中必不可少的资源,在同一个进程中的所有线程都共享地址空间线程间的大部分数据可以共享,并且相比较多进程而言,多线程间的通信开销小,启动速度快、占用资源少

二、多线程同步和互斥

多个线程同时执行任务,肯定会存在线程间的同步和互斥:

 1、线程同步:指的是线程之间所具有的一种制约关系,一个线程的执行依赖于另外一个线程的消息,当它没有得到另外一个线程的消息时应该等待,直到消息到达时才被唤醒。

2、线程互斥:指对于共享的进程系统资源,每个线程访问时的排他性。简单地说就是假设存在线程1和线程2都去使用当前进程的系统资源S时,线程1在使用这个系统资源时,线程2是不可以去同时去使用占用这个系统资源S的。

学术上点说:当有若干线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其他线程必须等待,直到占用资源者释放该资源。线程互斥可以被看作是一种特殊的线程同步。

三、线程间进行同步的方法

线程同步的方法可以被分为两类:

第一类:用户模式。注意使用时不需要切换内核态,只在用户态完成操作。

              临界区:适合一个进程内的多线程访问公共区域或代码段时使用。

第二类:内核模型。利用系统内核对象的单一性来进行同步,使用时需要切换内核和用户态。

               事件:通过线程间触发事件实现线程同步或线程互斥。

               互斥量:适合不同进程内多线程访问公共区域或代码段时使用,与临界区相似。

               信号量:与临界区和互斥量不同,可以实现多个线程同时访问公共区域数据,原理与操                                 作系统中PV操作类似,先设置一个访问公共区域的线程最大连接数,每有一个                               线程访问共享区,则其资源数就减一,直到资源数<=0。

四、多线程示例代码

C++11中提供了thread库管理线程、保护共享数据、线程间同步等功能。

头文件是#include<thread>

一个进程至少有一个线程,在c++中可以认为main函数就是我们的主线程。而在创建thread对象的时候,就是在这个线程之外创建了一个独立的子线程。只要创建了这个子线程并且开始运行了,主线程就完全和它没关系了,不知道CPU会什么时候调度它运行,什么时候结束运行!

那么如何创建、启动、结束多线程呢?

1、C++创建线程的三种方式

(1)通过一个初始函数创建线程

#include<iostream>
#include<thread>

using namespace std;


void Recognisethread()
{
	cout << "DL Recognise point cloud." << endl;
}


int main()
{
	thread task(Recognisethread);
	task.join();
	//task.detach();
	cout << "执行主线程" << endl;
	return 0;

}

运行结果如下:

 1)上面例子这种构建了一个thread对象task,构造的时候传递一个函数名进去,它就是这个线程的入口函数,函数执行完表示该线程也执行结束。

2)线程创建成功后就立即启动,并没有一个开始的API触发启动线程。

3)线程运行后需要显示的决定程序是否用join阻塞等待它完成,并回收该线程中使用的资源或者detach分离出去让它自行运作

(2)通过类对象创建线程

通过类来构造线程实际是仿函数的功能。仿函数是使用类来模拟函数调用行为,只要重载一个类的operator()方法,即可像调用函数一样来调用类。

提供给thread实例的函数对象会复制到新线程的存储空间中,函数对象的执行和调用都在线程的内存空间中执行


#include <iostream>
#include <thread>


class Recognise {
public:
    void operator()()
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        std::cout << "子线程延迟1s." << std::endl;
    }
};

int main()
{
    /* Recognise{}类似与Recognise rec{2.5, 3.5}这种列表初始化方式。Recognise{}定义了一个Recognise
    * 类的无名对象,并且初始化为空
    */
    std::thread task(Recognise{});
    task.detach();
    std::cout << "主线程结束" << std::endl;
    return 0;
}

运行结果如下:

注意:

1)这里面的类对象Recognise{}可以改为Recognise r;

2)可以明显看到,主线程太快了,还没等子线程运行就结束了。

3)join用于阻塞线程,所以主线程要等待子线程执行完后才会继续向下执行。调用join还会清理线程相关的存储部分,这表示join只能调用一次。(因为join已经清理掉了线程相关的内存,再次调用就会出现调用无效内存的这种低级错误!),此外可以使用joinable()函数判断是否可以调用join()函数。

4)detach用于主线程和当前线程分离,主线程可以先执行结束,如果主线程执行完了,子线程会在C++后台运行,一旦使用detach,与这个子线程关联的对象会失去对这个主线程的关联,此时这个子线程会驻留在C++后台运行,当主线程执行完毕结束,子线程会移交给C++运行时库管理,这个运行时库会清理与这个线程相关的资源(守护线程),detach会使子线程失去进程的控制;

5)开启线程后必须显示的决定主线程是等待还是分离子线程,否则会引起程序崩溃。

(3)通过Lambda表达式创建线程

#include <iostream>
#include <thread>

using namespace std;

int main()
{

    //lambda函数
    auto lambda_thread = [] {
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        std::cout << "子线程延迟1s" << std::endl;
    };
  
    std::thread task(lambda_thread);
    task.detach();
    std::cout << "主线程结束" << std::endl;
    return 0;
}

运行结果:

以上三种就是创建线程的方式。

线程如何调用类成员函数并传入参数?


#include <iostream>
#include <thread>


class LidarProcess {
public:
    void RecogniseThread(const PointCloudPtr &input_cloud_ptr)
{
        std::cout << "Process DL Inference." << std::endl;
    }
};
int main()
{
    LidarProcess lidar_process;
    PointCloudPtr inlier_cloud_ptr(new PointCloud);
    std::thread task(std::bind(&LidarProcess::RecogniseThread, this, inlier_cloud_ptr));
    task.join();
 
    return 0;
}

创建一个LidarProcess类,在主函数中将类LidarProcess中的成员函数绑定到线程对象task上。截取的工程代码,有些结构体定义未放出来,比如PointCloudPtr 是智能指针,所以在这就没有delete了。

多线程如何进行同步呢?

互斥量:互斥量是为了解决数据共享过程中可能存在的访问冲突问题。在c++11中提供4种互斥量如下。

1)非递归的互斥量

std::mutex;

2)带超时的非递归互斥量

std::timed_mutex;

3)递归互斥量

std::recursive_mutex;

4)带超时的递归互斥量

std::recursive_timed_mutex;

其中最常用的是mutex,通过实例化mutex创建互斥量。在进入临界区之前调用成员函数lock进行上锁,退出临界区时对互斥量unlock进行解锁。当一个线程使用特定互斥量锁住共享数据时,其他的线程想要访问锁住的数据,都必须等到之前那个线程对数据将进行解锁后,才能访问。(注意:这种方式需要在每个函数的出口或者异常都去调用lock)

mutex mx; //实例化一个mutex类的对象mx

vector<BBox> bboxes; //用于DL推理后存储Bounding Box的容器

vector<BBox> RecogniseObject(const PointCloudPtr input_cloud_ptr)
{...}

void RecogniseThread(const PointCloudPtr input_cloud_ptr)
{
  mu.lock();  //如果这句话抛出异常,mu永远会被锁住
  bboxes = RecogniseObject(input_cloud_ptr);
  mu.unlock();
}

因为通过lock与unclock可以解决线程之间的资源竞争问题,如果在RecogniseObject中执行识别时程序因为某些原因退出了,此时就无法unlock,这样其他线程也就无法获取mutex资源从而造成死锁现象,其实在加锁之前可以通过trylock()尝试一下能不能加锁。但更好的方式是采用lock_guard或者unique_lock来控制std::mutex

所以C++提供了一种RAII(Resource Acquisition Is Initialization 资源获取即初始化)方式的模板类lock_guard会在构造的时候提供加锁的互斥量,并在析构的时候进行解锁,从而保证了一个已锁的互斥量总是会被正确的解锁。(unique_lock类似)


mutex mx;            //实例化mutex
vector<BBox> bboxes; //用于DL推理后存储Bounding box的容器

vector<BBox> RecogniseObject(const PointCloudPtr input_cloud_ptr)
{...}

void RecogniseThread(const PointCloudPtr input_cloud_ptr)
{
  lock_guard<mutex> guard(mu); //当此句异常,mu对象自动被解锁
  bboxes = RecogniseObject(input_cloud_ptr);
}

大多数情况下,互斥量会和被保护的数据放在同一个类中,而不是定义成全局变量二者均定义成private成员互斥量保护的数据需要对接口的设计相当谨慎,要确保互斥量能锁住任何对保护数据的访问,尤其是需要传递指针和引用时

如何转移线程的所有权?

thread是可以移动的(movable),但不可复制(copyable)。移动拷贝或移动赋值都使得原有对象对所属资源的控制权发生转移。从对象A转移到对象B,对资源的控制只在对象B中保留。

在c++11标准库中,提供了std::move函数用于资源移动操作,来改变线程的所有权,从而灵活地决定线程在什么时候join或者detach。

thread task_1(RecogniseThread);
thread task_2(move(task_1));

注,以上两句代码:将线程从task_1转移给task_2,线程task_1就不再拥有线程的所有权,调用task_1.join()或者task_1.detach()会出现异常。要使用task_2来管理线程,此时task1的ID变为0,joinable变为为false。这也就意味着thread可以作为函数的返回类型,或者作为参数传递给函数,能够更为方便的管理线程。

图片

参考:

C++ 多线程编程 (qq.com)https://mp.weixin.qq.com/s?__biz=Mzg2NzUyMTc0Mw==&mid=2247484271&idx=1&sn=40c3c831df0dd24060cceb8e12816b23&chksm=cebb06cff9cc8fd97a3416dc43a8e6ac20c58cf576ec965c82213287d32ecec869a44f288e39&scene=178&cur_album_id=1761255140981800976#rd

(3条消息) C++11多线程join()和detach()的理解_Stone-CSDN博客_c++ join

【C++多线程】detach()及注意 - Chen沉尘 - 博客园 (cnblogs.com)

(1条消息) c++11中thread join和detach的区别_Keep Moving~-CSDN博客_c++ detach

​​​​​​(1条消息) c++11 使用detach()时,主线程和孤儿detach线程的同步控制_Trouble_provider的博客-CSDN博客_c++detach()

————————————————————分割线————————————————————

worker_ = std::make_shared<std::thread>(&InferController::worker, this, std::ref(pro));

&InferController::worker:普通指针 ,可以看作w。加上 std::make_shared转为智能指针

std::thread:线程类,可以看作thread

因此实际是thread* w。即上面整体上可以看作:定义了一个线程类的智能指针对象(也即是worker_),该智能指针worker_指向函数InferController::worker()

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值