网站片区,东营网站,品牌建设实施细则,潍坊seo招聘来源丨Qt教程 点击进入—3D视觉工坊学习交流群 在C学习过程中#xff0c;要想“更上一层楼”的话#xff0c;多线程编程是必不可少的一步#xff0c;前面的文章多半是基础方面的内容#xff0c;这节的话稍微有点拔高。所以说#xff0c;我们在看这篇文章的时候#x… 来源丨Qt教程 点击进入—3D视觉工坊学习交流群 在C学习过程中要想“更上一层楼”的话多线程编程是必不可少的一步前面的文章多半是基础方面的内容这节的话稍微有点拔高。所以说我们在看这篇文章的时候大家需要更多的思考是为什么这么做这样做的好处是什么以及多线程编程都可以应用在哪里话不多说跟着我一起认真探讨这块内容。 1、多线程 传统的CC11标准之前中并没有引入线程这个概念在C11出来之前如果我们想要在C中实现多线程需要借助操作系统平台提供的API比如Linux的或者windows下的。 C11提供了语言层面上的多线程包含在头文件中。它解决了跨平台的问题提供了管理线程、保护共享数据、线程间同步操作、原子操作等类。C11 新标准中引入了5个头文件来支持多线程编程如下图所示 1.1、多进程与多线程 多进程并发 使用多进程并发是将一个应用程序划分为多个独立的进程每个进程只有一个线程这些独立的进程间可以互相通信共同完成任务。由于操作系统对进程提供了大量的保护机制以避免一个进程修改了另一个进程的数据使用多进程比使用多线程更容易写出相对安全的代码。但是这也造就了多进程并发的两个缺点 在进程间的通信无论是使用信号、套接字还是文件、管道等方式其使用要么比较复杂要么就是速度较慢或者两者兼而有之。运行多个线程的开销很大操作系统要分配很多的资源来对这些进程进行管理。 当多个进程并发完成同一个任务时不可避免的是操作同一个数据和进程间的相互通信上述的两个缺点也就决定了多进程的并发并不是一个好的选择。所以就引入了多线程的并发。 多线程并发 多线程并发指的是在同一个进程中执行多个线程。 优点 有操作系统相关知识的应该知道线程是轻量级的进程每个线程可以独立的运行不同的指令序列但是线程不独立的拥有资源依赖于创建它的进程而存在。也就是说同一进程中的多个线程共享相同的地址空间可以访问进程中的大部分数据指针和引用可以在线程间进行传递。这样同一进程内的多个线程能够很方便的进行数据共享以及通信也就比进程更适用于并发操作。 缺点 由于缺少操作系统提供的保护机制在多线程共享数据及通信时就需要程序员做更多的工作以保证对共享数据段的操作是以预想的操作顺序进行的并且要极力的避免死锁(deadlock)。 1.2、多线程理解 单CPU内核的多个线程。 一个时间片运行一个线程的代码并不是真正意义的并行计算。 多个cpu或者多个内核 可以做到真正的并行计算。 1.3、创建线程 创建线程很简单只需要把函数添加到线程当中即可。 形式1 std::thread myThread ( thread_fun);
//函数形式为void thread_fun()
myThread.join();
//同一个函数可以代码复用创建多个线程 形式2 std::thread myThread ( thread_fun(100));
myThread.join();
//函数形式为void thread_fun(int x)
//同一个函数可以代码复用创建多个线程 形式3 std::thread (thread_fun,1).detach();
//直接创建线程没有名字
//函数形式为void thread_fun(int x) For Example 使用g编译下列代码的方式 g test.cc -o test -l pthread #include
#include
using namespace std;void thread_1()
{
cout子线程1endl;
}void thread_2(int x)
{coutx:xendl/x;
cout子线程2endl;
}int main()
{
thread first ( thread_1); // 开启线程调用thread_1()
thread second (thread_2,100); // 开启线程调用thread_2(100)
//thread third(thread_2,3);//开启第3个线程共享thread_2函数。
std::cout 主线程\n;first.join(); //必须说明添加线程的方式 second.join();
std::cout 子线程结束.\n;//必须join完成
return 0;
} 1.4、join与detach方式 当线程启动后一定要在和线程相关联的thread销毁前确定以何种方式等待线程执行结束。比如上例中的join。 detach方式启动的线程自主在后台运行当前的代码继续往下执行不等待新线程结束。join方式等待启动的线程完成才会继续往下执行。 可以使用joinable判断是join模式还是detach模式。 if (myThread.joinable()) foo.join(); 1join举例 下面的代码join后面的代码不会被执行除非子线程结束。 #include
#include
using namespace std;
void thread_1()
{while(1){//cout子线程1111endl; span/endl;}
}
void thread_2(int x)
{while(1){//cout子线程2222endl; span/endl;}
}
int main()
{thread first ( thread_1); // 开启线程调用thread_1()thread second (thread_2,100); // 开启线程调用thread_2(100)first.join(); // pauses until first finishes 这个操作完了之后才能destroyedsecond.join(); // pauses until second finishes//join完了之后才能往下执行。while(1){std::cout 主线程\n;}return 0;
} 2detach举例 下列代码中主线程不会等待子线程结束。如果主线程运行结束程序则结束。 #include
#include
using namespace std;void thread_1()
{while(1){cout子线程1111endl; span/endl;}
}void thread_2(int x)
{while(1){cout子线程2222endl; span/endl;}
}int main()
{thread first ( thread_1); // 开启线程调用thread_1()thread second (thread_2,100); // 开启线程调用thread_2(100)first.detach(); second.detach(); for(int i 0; i 10; i){std::cout 主线程\n;}return 0;
} 1.5、this_thread this_thread是一个类它有4个功能函数具体如下 函数使用说明get_idstd::this_thread::get_id()获取线程idyieldstd::this_thread::yield()放弃线程执行回到就绪状态sleep_forstd::this_thread::sleep_for(std::chrono::seconds(1));暂停1秒sleep_until如下一分钟后执行吗如下 using std::chrono::system_clock;
std::time_t tt system_clock::to_time_t(system_clock::now());
struct std::tm * ptm std::localtime(tt);
cout Waiting for the next minute to begin...\n;
ptm-tm_min; //加一分钟
ptm-tm_sec 0; //秒数设置为0//暂停执行到下一整分执行
this_thread::sleep_until(system_clock::from_time_t(mktime(ptm))); 2、mutex mutex头文件主要声明了与互斥量(mutex)相关的类。mutex提供了4种互斥类型如下表所示。 类型说明std::mutex最基本的 Mutex 类。std::recursive_mutex递归 Mutex 类。std::time_mutex定时 Mutex 类。std::recursive_timed_mutex定时递归 Mutex 类。 std::mutex 是C11 中最基本的互斥量std::mutex 对象提供了独占所有权的特性——即不支持递归地对 std::mutex 对象上锁而 std::recursive_lock 则可以递归地对互斥量对象上锁。 2.1、lock与unlock mutex常用操作 lock()资源上锁unlock()解锁资源trylock()查看是否上锁它有下列3种类情况 1未上锁返回false并锁住 2其他线程已经上锁返回true 3同一个线程已经对它上锁将会产生死锁。 死锁是指两个或两个以上的进程在执行过程中由于竞争资源或者由于彼此通信而造成的一种阻塞的现象若无外力作用它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁这些永远在互相等待的进程称为死锁进程。 下面结合实例对lock和unlock进行说明。 同一个mutex变量上锁之后一个时间段内只允许一个线程访问它。例如 #include// std::cout
#include// std::thread
#include// std::mutexstd::mutex mtx; // mutex for critical section
void print_block (int n, char c)
{
// critical section (exclusive access to std::cout signaled by locking mtx):mtx.lock();
for (int i0; i{std::cout c; }std::cout \n;mtx.unlock();
}
int main ()
{std::thread th1 (print_block,50,);//线程1打印*std::thread th2 (print_block,50,$);//线程2打印$th1.join();th2.join();
return 0;
} 输出 **************************************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ 如果是不同mutex变量因为不涉及到同一资源的竞争所以以下代码运行可 能会出现交替打印的情况或者另一个线程可以修改共同的全局变量 #include // std::cout
#include // std::thread
#include // std::mutexstd::mutex mtx_1; // mutex for critical section
std::mutex mtx_2; // mutex for critical section
int test_num 1;void print_block_1 (int n, char c)
{
// critical section (exclusive access to std::cout signaled by locking mtx):mtx_1.lock();
for (int i0; i{
//std::cout c;test_num 1;std::couttest_numstd/test_num::endl;}
std::cout \n;mtx_1.unlock();
}void print_block_2 (int n, char c)
{// critical section (exclusive access to std::cout signaled by locking mtx):mtx_2.lock();test_num 2;
for (int i0; i{
//std::cout c;test_num 2;std::couttest_numstd/test_num::endl;}mtx_2.unlock();
}int main ()
{
std::thread th1 (print_block_1,10000,*);
std::thread th2 (print_block_2,10000,$);th1.join();th2.join();
return 0;
} 2.2、lock_guard 创建lock_guard对象时它将尝试获取提供给它的互斥锁的所有权。当控制流离开lock_guard对象的作用域时lock_guard析构并释放互斥量。 lock_guard的特点 创建即加锁作用域结束自动析构并解锁无需手工解锁不能中途解锁必须等作用域结束才解锁不能复制 代码举例 #include
#include
#include
int g_i 0;
std::mutex g_i_mutex; // protects g_i用来保护g_ivoid safe_increment()
{
const std::lock_guardstd::mutex lock(g_i_mutex);g_i;
std::cout std::this_thread::get_id() : g_i \n;// g_i_mutex自动解锁}int main(){
std::cout main id: std::this_thread::get_id()std::endl;
std::cout main: g_i \n;std::thread t1(safe_increment);
std::thread t2(safe_increment);t1.join();t2.join();std::cout main: g_i \n;
} 说明 该程序的功能为每经过一个线程g_i 加1。因为涉及到共同资源g_i 所以需要一个共同mutexg_i_mutex。main线程的id为1所以下次的线程id依次加1。 2.3、unique_lock 简单地讲unique_lock 是 lock_guard 的升级加强版它具有 lock_guard 的所有功能同时又具有其他很多方法使用起来更加灵活方便能够应对更复杂的锁定需要。 unique_lock的特点 创建时可以不锁定通过指定第二个参数为std::defer_lock而在需要时再锁定可以随时加锁解锁作用域规则同 lock_grard析构时自动释放锁不可复制可移动条件变量需要该类型的锁作为参数此时必须使用unique_lock 所有 lock_guard 能够做到的事情都可以使用 unique_lock 做到反之则不然。那么何时使lock_guard呢很简单需要使用锁的时候首先考虑使用 lock_guard因为lock_guard是最简单的锁。 下面是代码举例 #include
#include
#include
struct Box {
explicit Box(int num) : num_things{num} {}
int num_things;
std::mutex m;
};
void transfer(Box from, Box to, int num)
{
// defer_lock表示暂时unlock默认自动加锁
std::unique_lockstd::mutex lock1(from.m, std::defer_lock);
std::unique_lockstd::mutex lock2(to.m, std::defer_lock);//两个同时加锁
std::lock(lock1, lock2);//或者使用lock1.lock()from.num_things - num;to.num_things num;//作用域结束自动解锁,也可以使用lock1.unlock()手动解锁
}
int main()
{
Box acc1(100);
Box acc2(50);std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);t1.join();t2.join();
std::cout acc1 num_things: acc1.num_things std::endl;
std::cout acc2 num_things: acc2.num_things std::endl;
} 说明 该函数的作用是从一个结构体中的变量减去一个num加载到另一个结构体的变量中去。std::mutex m;在结构体中mutex不是共享的。但是只需要一把锁也能锁住因为引用传递后同一把锁传给了两个函数。cout需要在join后面进行要不然cout的结果不一定是最终算出来的结果。std::ref 用于包装按引用传递的值。std::cref 用于包装按const引用传递的值。 3、condition_variable condition_variable头文件有两个variable类一个是condition_variable另一个是condition_variable_any。condition_variable必须结合unique_lock使用。condition_variable_any可以使用任何的锁。下面以condition_variable为例进行介绍。 condition_variable条件变量可以阻塞wait、wait_for、wait_until调用的线程直到使用notify_one或notify_all通知恢复为止。condition_variable是一个类这个类既有构造函数也有析构函数使用时需要构造对应的condition_variable对象调用对象相应的函数来实现上面的功能。 类型说明condition_variable构建对象析构删除waitWait until notifiedwait_forWait for timeout or until notifiedwait_untilWait until notified or time pointnotify_one解锁一个线程如果有多个则未知哪个线程执行notify_all解锁所有线程cv_status这是一个类表示variable 的状态如下所示 enum class cv_status { no_timeout, timeout }; 3.1、wait 当前线程调用 wait() 后将被阻塞(此时当前线程应该获得了锁mutex不妨设获得锁 lck)直到另外某个线程调用 notify_* 唤醒了当前线程。 在线程被阻塞时该函数会自动调用 lck.unlock() 释放锁使得其他被阻塞在锁竞争上的线程得以继续执行。另外一旦当前线程获得通知(notified通常是另外某个线程调用 notify_* 唤醒了当前线程)wait()函数也是自动调用 lck.lock()使得lck的状态和 wait 函数被调用时相同。 代码示例 #include // std::cout
#include // std::thread, std::this_thread::yield
#include // std::mutex, std::unique_lock
#include // std::condition_variablestd::mutex mtx;
std::condition_variable cv;
int cargo 0;
bool shipment_available()
{
return cargo!0;
}
void consume (int n)
{
for (int i0; i{
std::unique_lockstd::mutex lck(mtx);//自动上锁
//第二个参数为false才阻塞wait阻塞完即unlock给其它线程资源cv.wait(lck,shipment_available);// consume:
std::cout cargo \n;cargo0;}
}
int main ()
{
std::thread consumer_thread (consume,10);
for (int i0; i10; i) {
//每次cargo每次为0才运行。while (shipment_available()) std::this_thread::yield();
std::unique_lockstd::mutex lck(mtx);cargo i1;cv.notify_one();}consumer_thread.join();return 0;
} 说明 主线程中的while每次在cargo0才运行。每次cargo被置为0会通知子线程unblock(非阻塞)也就是子线程可以继续往下执行。子线程中cargo被置为0后wait又一次启动等待。也就是说shipment_available为false则等待。 3.2、wait_for 与std::condition_variable::wait() 类似不过 wait_for可以指定一个时间段在当前线程收到通知或者指定的时间 rel_time 超时之前该线程都会处于阻塞状态。而一旦超时或者收到了其他线程的通知wait_for返回剩下的处理步骤和 wait()类似。 template class Rep, class Period
cv_status wait_for (unique_lock lck,const chrono::duration rel_time); 另外wait_for 的重载版本的最后一个参数pred表示 wait_for的预测条件只有当 pred条件为false时调用 wait()才会阻塞当前线程并且在收到其他线程的通知后只有当 pred为 true时才会被解除阻塞。 template class Rep, class Period, class Predicate
bool wait_for (unique_lock lck,const chrono::duration rel_time, Predicate pred); 代码示例 #include// std::cout
#include// std::thread
#include// std::chrono::seconds
#include// std::mutex, std::unique_lock
#include// std::condition_variable, std::cv_statusstd::condition_variable cv;
int value;
void read_value()
{
std::cin value;cv.notify_one();
}
int main ()
{
std::cout Please, enter an integer (Ill be printing dots): \n;
std::thread th (read_value);std::mutex mtx;
std::unique_lockstd::mutex lck(mtx);
while (cv.wait_for(lck,std::chrono::seconds(1))std::cv_status::timeout) {
std::cout . std::endl;}
std::cout You entered: value \n;th.join();
return 0;
} 通知或者超时都会解锁所以主线程会一直打印。示例中只要过去一秒就会不断的打印。 4、线程池 4.1、概念 在一个程序中如果我们需要多次使用线程这就意味着需要多次的创建并销毁线程。而创建并销毁线程的过程势必会消耗内存线程过多会带来调动的开销进而影响缓存局部性和整体性能。 线程的创建并销毁有以下一些缺点 创建太多线程将会浪费一定的资源有些线程未被充分使用。销毁太多线程将导致之后浪费时间再次创建它们。创建线程太慢将会导致长时间的等待性能变差。销毁线程太慢导致其它线程资源饥饿。 线程池维护着多个线程这避免了在处理短时间任务时创建与销毁线程的代价。 4.2、线程池的实现 因为程序边运行边创建线程是比较耗时的所以我们通过池化的思想在程序开始运行前创建多个线程这样程序在运行时只需要从线程池中拿来用就可以了大大提高了程序运行效率 一般线程池都会有以下几个部分构成 线程池管理器ThreadPoolManager:用于创建并管理线程池也就是线程池类工作线程WorkThread: 线程池中线程任务队列task: 用于存放没有处理的任务。提供一种缓冲机制。append用于添加任务的接口 线程池实现代码 #ifndef _THREADPOOL_H
#define _THREADPOOL_H
#include
#include
#include
#include
#include
#include
#include//unique_ptr
#include
const int MAX_THREADS 1000; //最大线程数目
template typename T
class threadPool
{
public:threadPool(int number 1);//默认开一个线程~threadPool();
std::queuetasks_queue; //任务队列
bool append(T *request);//往请求队列task_queue中添加任务
private:
//工作线程需要运行的函数,不断的从任务队列中取出并执行
static void *worker(void arg);
void run();
private:
std::vectorstd::thread work_threads; //工作线程std::mutex queue_mutex;
std::condition_variable condition; //必须与unique_lock配合使用
bool stop;
};//end class//构造函数创建线程
template typename T
threadPool::threadPool(int number) : stop(false)
{
if (number 0 || number MAX_THREADS)
throw std::exception();
for (int i 0; i number; i){
std::cout created Thread num is : i std::endl;work_threads.emplace_back(worker, this);
//添加线程
//直接在容器尾部创建这个元素省去了拷贝或移动元素的过程。}
}
template typename T
inline threadPool::~threadPool()
{std::unique_lockstd::mutex lock(queue_mutex);stop true;condition.notify_all();
for (auto ww : work_threads)ww.join();//可以在析构函数中join
}
//添加任务
template typename T
bool threadPool::append(T *request)
{//操作工作队列时一定要加锁因为他被所有线程共享queue_mutex.lock();//同一个类的锁tasks_queue.push(request);queue_mutex.unlock();condition.notify_one(); //线程池添加进去了任务自然要通知等待的线程
return true;
}//单个线程
template typename T
void threadPool::worker(void *arg)
{threadPool pool (threadPool *)arg;pool-run();//线程运行
return pool;
}
template typename T
void threadPool::run()
{
while (!stop)
{
std::unique_lockstd::mutex lk(this-queue_mutex);
/* unique_lock() 出作用域会自动解锁 /this-condition.wait(lk, [this] { return !this-tasks_queue.empty(); });//如果任务为空则wait就停下来等待唤醒//需要有任务才启动该线程不然就休眠if (this-tasks_queue.empty())//任务为空双重保障{ assert(0断了);//实际上不会运行到这一步因为任务为空wait就休眠了。continue;}else{T *request tasks_queue.front();tasks_queue.pop();if (request)//来任务了开始执行request-process();}}
}
#endif 说明 构造函数创建所需要的线程数一个线程对应一个任务任务随时可能完成线程则可能休眠所以任务用队列queue实现线程数量有限线程用采用wait机制。任务在不断的添加有可能大于线程数处于队首的任务先执行。只有添加任务(append)后才开启线程condition.notify_one()。wait表示任务为空时则线程休眠等待新任务的加入。添加任务时需要添加锁因为共享资源。 测试代码 #include mythread.h
#include
#include
using namespace std;
class Task
{
public:
void process()
{
//cout run......... endl;//测试任务数量
long i1000000;
while(i!0){
int j sqrt(i);i--;}}
};
int main(void){threadPoolpool(6);//6个线程vector
std::string str;
while (1){Task *tt new Task();//使用智能指针pool.append(tt);//不停的添加任务任务是队列queue因为只有固定的线程数cout添加的任务数量pool.tasks_queue.size()endl/pool.tasks_queue.size(); delete tt;}
} 本文仅做学术分享如有侵权请联系删文。 点击进入—3D视觉工坊学习交流群 干货下载与学习 后台回复巴塞罗那自治大学课件即可下载国外大学沉淀数年3D Vison精品课件 后台回复计算机视觉书籍即可下载3D视觉领域经典书籍pdf 后台回复3D视觉课程即可学习3D视觉领域精品课程 3D视觉工坊精品课程官网3dcver.com 1.面向自动驾驶领域的3D点云目标检测全栈学习路线(单模态多模态/数据代码)2.彻底搞透视觉三维重建原理剖析、代码讲解、及优化改进3.国内首个面向工业级实战的点云处理课程4.激光-视觉-IMU-GPS融合SLAM算法梳理和代码讲解5.彻底搞懂视觉-惯性SLAM基于VINS-Fusion正式开课啦6.彻底搞懂基于LOAM框架的3D激光SLAM: 源码剖析到算法优化7.彻底剖析室内、室外激光SLAM关键算法原理、代码和实战(cartographerLOAM LIO-SAM) 8.从零搭建一套结构光3D重建系统[理论源码实践] 9.单目深度估计方法算法梳理与代码实现 10.自动驾驶中的深度学习模型部署实战 11.相机模型与标定(单目双目鱼眼 12.重磅四旋翼飞行器算法与实战 13.ROS2从入门到精通理论与实战 14.国内首个3D缺陷检测教程理论、源码与实战 15.基于Open3D的点云处理入门与实战教程 16.透彻理解视觉ORB-SLAM3理论基础代码解析算法改进 17.机械臂抓取从入门到实战 重磅粉丝学习交流群已成立 交流群主要有3D视觉、CV深度学习、SLAM、三维重建、点云后处理、自动驾驶、多传感器融合、CV入门、三维测量、VR/AR、3D人脸识别、医疗影像、缺陷检测、行人重识别、目标跟踪、视觉产品落地、视觉竞赛、车牌识别、硬件选型、ORB-SLAM系列源码交流、深度估计、TOF、求职交流等方向。 扫描以下二维码添加小助理微信dddvisiona,一定要备注研究方向学校/公司昵称例如”3D视觉 上海交大 静静“。请按照格式备注可快速被通过且邀请进群。原创投稿也请联系。 ▲长按加微信群或投稿微信号dddvisiona 3D视觉从入门到精通知识星球针对3D视觉领域的视频课程三维重建系列、三维点云系列、结构光系列、手眼标定、相机标定、激光/视觉SLAM、自动驾驶等、源码分享、知识点汇总、入门进阶学习路线、最新paper分享、疑问解答等进行深耕更有各类大厂的算法工程人员进行技术指导。与此同时星球将联合知名企业发布3D视觉相关算法开发岗位以及项目对接信息打造成集技术与就业为一体的铁杆粉丝聚集区6000星球成员为创造更好的AI世界共同进步知识星球入口 学习3D视觉核心技术扫描查看3天内无条件退款 高质量教程资料、答疑解惑、助你高效解决问题 觉得有用麻烦给个赞和在看~