C++中的多线程问题

进程和线程

进程和线程的关系

应用程序>程序=进程>线程

一个应用程序(APP)可以有多个进程,彼此相互独立。一个进程可以开启多个线程,相当于是分工完成进程任务。

比如一个辅瞄程序是一个进程,完成辅瞄过程中的所有任务;(辅瞄代码中的)相机任务是一个线程,只负责从相机(或本地视频)读取视频流,供其他线程使用。

进程和线程的区别

各个进程拥有独立的虚拟地址空间,也就是说进程间的数据相互隔离,不能直接访问。

同一进程的各个线程共享同一个虚拟地址空间,也就是一个线程可以直接访问另一个线程中的变量(如果知道地址)。

物理内存地址:实际硬件中的空间地址。

虚拟内存地址:程序中使用的内存地址。

​ 操作系统为每个进程分配一块独立的虚拟内存,通过分段技术,由段选择子和段内偏移地址共同决定实际的物理地址。

单核并发和多核并发

单核并发

伪并发。

以前的计算机系统中只有一颗CPU,且只有一个核心。在单核CPU上执行并发程序时,通过“任务切换”来模拟并发执行。

大部分情况下单核并发不会提速,反而会降速,因为CPU在切换上下文的过程中也会占用资源。

但是在某些特定情境下是非常有用的,比如在执行IO的时候,IO的线程被挂起,CPU去执行其他线程的操作,而不用一直阻塞等待IO结束。

多核并发

真并发。

每个线程执行在单独的核心上(所以核心数也会对线程数有限制),各个线程之间就不需要上下文切换了。

请添加图片描述

并发的分类

任务并发:也是通常理解的并发。一个任务划分为多个子任务。

数据并发:将数据分组,在不同组的数据上执行相同的操作。比如将图像分块,在不同块上同时跑检测线程。

C++并发库
  • pthread:C语言风格

感觉细粒度小,可操作空间大。

  • thread:C++ STL

基于pthread开发,用起来很方便,也很规范。实现还是偏底层的,速度也比较快。

内存模型

线程中变量存储模型

每个线程有独立的线程上下文,包括线程的栈空间和栈相关的一些通用寄存器(比如ebp,eip,cs等),用于存储局部变量、函数参数等。这些数据在每个线程中会有单独的副本,也是其他线程不能直接访问的。

当然,上面提到了,各个线程间也会共享进程的虚拟地址空间,所以包括全局变量和静态局部变量等,在线程中都只有唯一副本。也就是说,多个线程都需要用到的数据可以考虑初始化为全局变量。

但是由于各线程在同一虚拟地址空间中,所以不同线程内存空间是不设防的,可以通过参数传递等方式共享线程上下文中的数据。

创建线程和线程属性

  • thread
    • thread t1(func, params):创建线程,执行func函数,传入params
    • thread t2(&A::func, &a, params):创建线程,执行a.func,传入params
  • join
    • t.join():阻塞当前线程,等待t线程执行完成之后继续。
  • detach
    • t.detach():分离t线程,二者不再相关。如果此时t线程中使用了共享数据,可能出现未定义的错误。

共享数据和互斥锁

共享数据

共享数据指可能被多个线程同时访问的数据。可以说数据共享是多线程存在的意义,但是数据共享的便捷性也会带来诸如数据竞争等一系列的问题。

数据竞争是指线程在读取某一共享变量的过程中,有一个或多个其他线程对该共享变量进行修改。这就使得读取到的是变量的中间状态,可能会引起未定义的错误。

互斥锁
  • mutex

    通过互斥锁可以维护各个线程访问共享数据的顺序,一个互斥锁同一时间只能被一个线程获取,其他线程如果尝试获取锁,则会被阻塞,直到锁被释放之后才会获取锁。

    互斥锁实际上并非是对数据上锁,只是对“锁”上锁。也就是说,如果一个线程使用锁,另一个线程不使用锁,或者使用另一个锁,那么此时互斥锁就失效了,没有起到防止数据竞争的效果。

    同时,使用互斥锁也会带来死锁等问题。

    mtx.lock()

    mtx.unlock()

  • lock_guard

    lock_guard会在变量初始化的时候自动上锁,析构的时候自动释放。

    lock_guard<mutex> lock(mtx)

  • unique_lock

    unique_lock是plus版的lock_guard,提供了暂时解锁的操作。

    unique_lock<mutex> lock(mtx)

原子类型

C++11专门提供了一个新的泛型atomic,该类型变量默认只能被一个线程访问。

如果只是单个变量需要上锁,可以考虑这种方法

atmoic<T> value;

线程协同和条件变量

线程同步

在辅瞄多线程的任务中,不同线程之间不是完全独立的。理想情况下,相机线程刚读取完一张图像之后,检测线程就立即检测装甲,同时在相机读取图像的过程中不会一直阻塞等待,而是将CPU资源让给其他的任务。这就需要用到所谓的线程同步技术。

线程同步有很多种操作,使用比较广泛的是条件变量。当某个线程已经确定条件得到满足后,会通知一个或多个其他线程去执行在条件变量上等待的线程。

  • condition_variable

    condition_variable有wait和notify_one / / /all()操作。

    wait操作用于待同步线程,一般结合lambda表达式一起用(也可以直接用个while):

    cv.wait(lock, [&]()->bool {return flag;}),表示flag为false时阻塞

    notify_one / / /all操作用于唤醒在等待的线程

    cv.notify_one(),唤醒+flag为true时,继续执行

demo:

// producer
unique_lock<mutex> lock(mtx);
...
flag = true;
cv.notify_all();
lock.unlock();

// consumer
unique_lock<mutex> lock(mtx);
cv.wait(lock, []()->bool{return flag;});
flag = false;
...
lock.unlock();

辅瞄中多线程的优化和helgrind

helgrind

helgrind是一个valgrind工具,用于检测基于PISIX(pthread)多线程的同步错误

helgrind可以检测出三种错误:

  1. api误用
  2. 死锁
  3. 数据竞争

在分析helgrind检测报告的过程中,发现了一个数据竞争的问题:

之前因为相机线程和检测线程间数据传输图像速度慢(1e6次计算量,差不多10ms,都跟跑一次检测差不多了),所以改成传递图像指针。即为图像分配一块内存,每次直接对内存数据做修改,然后传递指向内存的指针即可,使用图像的时候用常引用取出即可。实测是可以省10ms的。

但是这又引发了一个问题:在修改之后,原来对图像拷贝的操作变成了对指针拷贝的操作,所以此时互斥锁并没有起到保护图像数据的功能,只是保护了指针。图像内存中数据的修改并没有被保护,引发了大量的数据竞争。

现在大概的思路是使用一个滚动数组创建一个图像池,保存相机读取到的图像,每次取出正在修改的图像的前一帧将其指针送给检测线程,传递的时候使用一个二级指针即可。

constexpr int size = 2;
Mat frame[size];

// camera
static int id = 0;
video >> frame[id];
frame_p = &frame[(id-1)&1];
frame_pp = &frame_p;

//detector
const Mat& frame = **frame_pp;

优化率50%+

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值