并发线程(21)——线程池
文章目录
- 二十一、day21
- 1. 线程池实现
- 1.1 完整代码
- 1.2 解释
二十一、day21
我们之前在学习std::future、std::async、std::promise相关的知识时,通过std::promise和packaged_task构建了一个可用的线程池,可参考文章:并发编程(6)——future、promise、async,线程池 | 爱吃土豆的个人博客。但只是将代码给出,简单做了介绍,从本节开始,将从头开始学习如何通过构建线程池以及一些其他和线程池有关的知识。
参考:
恋恋风辰官方博客
【C++】线程池 - AirCL - 博客园
线程池工作原理和实现 - 【C语言版 】C/C++_哔哩哔哩_bilibili
基于C++11实现的异步线程池【C/C++】_哔哩哔哩_bilibili
1. 线程池实现
1.1 完整代码
#ifndef __THREAD_POOL_H__
#define __THREAD_POOL_H__
#include <atomic>
#include <condition_variable>
#include <future>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>class NoneCopy {
public:~NoneCopy() {}
protected:NoneCopy() {}
private:NoneCopy(const NoneCopy&) = delete;NoneCopy& operator=(const NoneCopy&) = delete;
};class ThreadPool : public NoneCopy {
public:// delte拷贝构造和拷贝赋值// ThreadPool(const ThreadPool&) = delete;// ThreadPool& operator=(const ThreadPool&) = delete;// 简单单例模式的实现static ThreadPool& instance() {static ThreadPool ins;return ins;}// 对任务(不接受参数并返回 void 的可调用对象)进行包装的包装器,用Task作为别名using Task = std::packaged_task<void()>;// 析构函数~ThreadPool() {stop();}// 该函数用于插入新任务至队列,并返回新任务的futuretemplate <class F, class... Args>auto commit(F&& f, Args&&... args) ->std::future<decltype(std::forward<F>(f)(std::forward<Args>(args)...))> {using RetType = decltype(f(args...)); // 使用RetType作为可调用对象返回类型的别名if (stop_.load()) // 线程池是否处于关闭状态return std::future<RetType>{};// 将可调用对象和参数用装饰器packaged_task进行包装auto task = std::make_shared<std::packaged_task<RetType()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));// 从包装器获取futurestd::future<RetType> ret = task->get_future();// 在{}内使用lock_guard锁定互斥量,并将task使用emplace插入至任务队列{std::lock_guard<std::mutex> cv_mt(cv_mt_);tasks_.emplace([task] { (*task)(); });}cv_lock_.notify_one();return ret;}// 返回线程数量int idleThreadCount() {return thread_num_;}
private:ThreadPool(unsigned int num = std::thread::hardware_concurrency()): stop_(false) {{if (num <= 1)thread_num_ = 2;elsethread_num_ = num;}start();}// 启动线程池void start() {for (int i = 0; i < thread_num_; ++i) {// 使用容器存储线程对象pool_.emplace_back([this]() { // 每个线程执行的lamda函数(线程池执行的任务相同)while (!this->stop_.load()) {Task task;{std::unique_lock<std::mutex> cv_mt(cv_mt_);this->cv_lock_.wait(cv_mt, [this] {return this->stop_.load() || !this->tasks_.empty();});if (this->tasks_.empty())return;task = std::move(this->tasks_.front());this->tasks_.pop();}this->thread_num_--;task();this->thread_num_++;}});}}// 将线程池关闭void stop() {stop_.store(true); // 将判断变量置为truecv_lock_.notify_all(); // 将所有线程唤醒for (auto& td : pool_) {if (td.joinable()) { // 打印线程池中所有的线程idstd::cout << "join thread " << td.get_id() << std::endl;td.join();}}}
private:std::mutex cv_mt_; // 互斥量std::condition_variable cv_lock_; // 条件变量std::atomic_bool stop_; // 布尔类型,配合条件变量使用std::atomic_int thread_num_; // 线程数量std::queue<Task> tasks_; // 任务队列,每个任务使用packaged_task进行包装std::vector<std::thread> pool_; // 使用容器存储线程
};
#endif // !__THREAD_POOL_H__
1.2 解释
1)首先我们需要设计一个基类,所有继承此基类的所有类均会 delete 拷贝构造和拷贝赋值(如果父类 delete、default、explicit构造函数,那么同样会作用于子类),基类 NoneCopy 定义如下:
class NoneCopy {
public:~NoneCopy(){}
protected:NoneCopy(){}
private:NoneCopy(const NoneCopy&) = delete;NoneCopy& operator=(const NoneCopy&) = delete;
};class ThreadPool : public NoneCopy {}
当 ThreadPool 类继承基类 NoneCopy 后,会自动继承父类的拷贝控制属性,ThreadPool 的拷贝构造和拷贝赋值会隐式 delete。而且我们需要将 ThreadPool 的默认构造函数设置为 private 或 protected,确保外部无法直接调用 ThreadPool() 来创建实例,只能通过 instance() 方法获取唯一实例。
2)因为线程池不能被拷贝也不能被赋值,并且必须是单例模型,所以接下来我们实现 ThreadPool 的单例,我们在前文说过,单例模式有三种实现方式:1. 通过std::call_once和std::once_flag实现;2. 通过静态成员函数实现;3. 智能指针双重检测实现。区别在于1和3可以显式定义删除器,而2没办法定义删除器,但是2的实现方式最简单。我们这里使用第二种方式即可:
// 简单单例模式的实现
static ThreadPool& instance() {static ThreadPool ins;return ins;
}
但注意,该方法只能在C++11及以后的平台才可以使用,因为返回局部静态变量只有在C++11及以上是线程安全的
3)在线程池中,我们需要将任务存储至任务队列中,但因为队列要求存储数据元素的类型相同,我们这里必须定义任务的类型为:
using Task = std::packaged_task<void()>;
std::queue<Task> tasks_;
tasks_.emplace([task] { (*task)(); });
队列使用 STL 的 queue 即可。任务队列 tasks_的元素类型是std::packaged_task<void()>,但为什么后面我们在 commit 函数中插入任务时插入的是一个lambda函数?
-
std::packaged_task<void()>:std::packaged_task<void()>表示packaged_task接受任何无传递参数并返回void的可调用对象。它可以封装任何符合这个签名的可调用对象,包括普通函数、函数对象、和 lambda 表达式。 -
lambda 表达式:
[task] { (*task)(); },这个表达式本身是一个可调用对象。它捕获了外部变量task,并在调用时执行(*task)(),这实际上是调用了packaged_task封装的函数。 -
插入到队列:
在使用
tasks_.emplace(...)时,实际上在构造一个std::function<void()>类型的对象,而这个对象可以被std::packaged_task接受。由于 lambda 表达式符合
void()的函数签名,因此std::packaged_task能够接受它并正确地存储。
std::packaged_task重载了operator()运算符,并通过重载的operator()来执行包装的可调用对象。比如在命令std::packaged_task<void(int)> task(myFunction);中,执行task()其实就算再执行myFunction()。所以在lambda函数中(*task)()其实是在调用可执行函数,只不过我们将其封装到了一个无返回值且无参数的lambda函数中,这是为了所有类型的可调用对象都可以通过一个无返回值且无参数的lambda函数封装,以便存储至任务队列中。
4)ThreadPool 的其他私有成员定义如下:
std::mutex cv_mt_; // 互斥量std::condition_variable cv_lock_; // 条件变量std::atomic_bool stop_; // 布尔类型,配合条件变量使用std::atomic_int thread_num_; // 线程数量std::queue<Task> tasks_; // 任务队列,每个任务使用packaged_task进行包装std::vector<std::thread> pool_; // 使用容器存储线程
thread_num_是线程池当前可用线程的数量,当我们调用一个线程执行任务时,thread_num_会减一,当任务完成后会加一,保证线程池不超负载。pool_用于存储线程,因为线程的创建和销毁存在一定的开销,我们需要将线程存放至vector中,以便复用线程。当线程中的任务执行完毕后,线程池会进入 “可联结状态”(joinable),但std::thread对象本身不会自动销毁,仍旧保留在 vector 中,让线程处于空闲状态,等待下一个任务,以便复用。stop_用于判断线程池是否被销毁,默认是False,当stop_ == True时,线程池销毁,此时会将任务队列和线程容器全部销毁(等所有任务执行完毕后才会销毁)。
5)ThreadPool 的构造函数被设置为 private,这是为了确保外部无法直接调用 ThreadPool() 来创建实例,只能通过 instance() 方法获取唯一实例:
ThreadPool(unsigned int num = std::thread::hardware_concurrency()): stop_(false) {{if (num <= 1)thread_num_ = 2;elsethread_num_ = num;}start();
}
std::thread::hardware_concurrency()是thread类是一个静态方法,用于获取当前计算机的CPU核心数,我们可以根据这个结果在线程池中创建出数量相等的线程,每个线程独自占有一个CPU核心,这些线程就不用分时复用CPU时间片,此时程序的并发效率是最高的。
6)start 函数的主要功能是启动线程,并将线程存储到一个 vector 中进行管理。在线程的回调函数中(lambda 函数执行过程中),线程的主要任务是从任务队列中取出任务并执行。如果队列中有任务,线程会弹出任务并执行;如果队列为空,线程将挂起,等待新的任务被添加被通知(notify_one())。这种实现通过条件变量来避免线程在无任务时的忙等问题,从而有效减少资源浪费。而部分初学者在实现线程池时,可能会采用简单的循环检查方式(即当任务队列为空时,线程会不断循环检查队列状态)。这种方式容易导致线程忙等,占用大量 CPU 资源,进而造成性能下降和资源浪费。实现代码如下:
void start() {for (int i = 0; i < thread_num_; ++i) {// 使用容器存储线程对象pool_.emplace_back([this]() { // 每个线程执行的lamda函数(线程池执行的任务相同)while (!this->stop_.load()) { // 判断线程池是否停止Task task; // 初始化一个接受无参并无返回值可调用对象的packaged_task{std::unique_lock<std::mutex> cv_mt(cv_mt_);// 条件变量判断,当满足任务队列不为空或者stop_为true,并且当前线程被唤醒时,退出挂起状态this->cv_lock_.wait(cv_mt, [this] {return this->stop_.load() || !this->tasks_.empty();});// 队列为空,那么就只有stop_为true一种情况,此时无任务需要处理,直接退出if (this->tasks_.empty())return;// 处理队列剩余任务task = std::move(this->tasks_.front());this->tasks_.pop();}this->thread_num_--; // 减少一个线程数用来执行task任务task(); // 执行任务this->thread_num_++; // task任务在线程内是同步执行的,所以当task任务执行完后,可用的线程数加一}});}
}
线程池会启动数量为thread_num_的线程,每个线程执行以下程序:
- 将当前线程插入至
pool_容器内进行管理 - 循环判断线程池是否关闭,如果不关闭,那么该线程将会无止境的进行工作
- 为了避免while循环占用CPU资源,使用条件变量挂起当前当前,直至满足(任务队列不为空或者
stop_为true),并且当前线程被唤醒时,退出挂起状态。挂起状态时,当前线程会释放锁让其他线程可以访问共享资源;线程被唤醒时,当前线程会重新拿取锁- 如果队列为空,且当前线程被唤醒,那就只有一种可能:线程池关闭。此时无任务需要处理,直接退出
- 如果队列不为空,取出任务队列的第一个元素
- 因为在当前线程内的任务执行是同步的,所以在执行任务前需要将可用线程数减一,待执行完任务后,可用线程数加一。
- 最后,循环第二步~第四步
7)当线程池被销毁时,必须等待线程池中的所有任务执行完毕后才可以销毁资源:
~ThreadPool() {stop();
}// 将线程池关闭
void stop() {stop_.store(true); // 将判断变量置为truecv_lock_.notify_all(); // 将所有线程唤醒for (auto& td : pool_) {if (td.joinable()) { // 打印线程池中所有的线程idstd::cout << "join thread " << td.get_id() << std::endl;td.join();}}
}
stop函数中我们将停止标记设置为true,并且调用条件变量的notify_all唤醒所有线程,被唤醒的线程获取任务队列中的任务,如果队列为空,则该线程直接返回,其他线程继续执行任务,直至所有任务结束,销毁资源。
8) 接下来我们需要封装一个接口用于投递任务给线程池,我们在上面说过,我们需要将任务存储至任务队列中,但因为队列要求存储数据元素的类型相同,我们这里必须定义任务的类型。我们定义了线程池执行的任务类型为 void(void) 的可调用对象,但是现实情况是存在不同类型的任务,返回类型已经参数类型军可能不同,我们应该如何将其封装为一个 void(void) 对象而不影响其使用功能?我们在一开始简单的说了可以通过 packaged_task 重载的 () 运算符进行调用函数,但具体过程需要详细说明。
我们可以通过 bind 将一个函数绑定为 void(void) 类型:
int functionint(int param) {std::cout << "param is " << param << std::endl;return 0;
}
void bindfunction() {std::function<int(void)> functionv = std::bind(functionint, 3);functionv();
}
假如我们的可执行任务是 functionint,它接受一个类型为 int 的参数,返回 int 类型,那么我们可以通过 std::bind 以及 std::function 将其绑定为一个 std::function<int(void)> 对象,我们通过执行 std::function 对象即可执行可执行函数,而且 std::function 是类型是 int(void)。因为我们这里手动给 functionint 了一个参数 3,所以 functionint 的参数类型是 void,但是它的返回类型仍然是 int。而但我们的任务队列要放入返回值为void,参数也为void的函数,该怎么办呢?
我们可以通过 lambda 生成一个返回值和参数都为 void 的函数,函数内部调用 functionv 即可,我们将上面的函数functionint和调用的参数3打包放入任务队列:
void pushtasktoque() {std::function<int(void)> functionv = std::bind(functionint, 3);using Task = std::packaged_task<void()>;std::queue<Task> taskque;taskque.emplace([functionv]() {functionv();});
}
因为我们是通过一个类型为void(void)的 lambda 表达式间接调用的可调用对象,我们可以将这个 lambda 投递至任务队列。
当任务执行完成后,由 packaged_task 封装的任务会返回 future 对象,我们可以通过调用 future 对象的 get() 或 wait() 函数获取任务结果。修改上面的代码,使其返回带有任务结果的 future 对象:
std::future<int> committask() {std::function<int(void)> functionv = std::bind(functionint, 3);auto taskf = std::make_shared<std::packaged_task<int(void)>>(functionv);auto res = taskf->get_future();using Task = std::packaged_task<void()>;std::queue<Task> taskque;taskque.emplace([taskf]() {(*taskf)();});return res;
}
我们将上面这个函数封装为一个适用于多种情况的模板函数,为了保证对象类型的不变,使用 C++ 的完美转发:
template <class F, class... Args>
auto commit(F&& f, Args&&... args) -> std::future<decltype(std::forward<F>(f)(std::forward<Args>(args)...))> {using RetType = decltype(f(args...)); // 使用RetType作为可调用对象返回类型的别名if (stop_.load()) // 线程池是否处于关闭状态return std::future<RetType>{};// 将可调用对象和参数用装饰器packaged_task进行包装auto task = std::make_shared<std::packaged_task<RetType()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));// 从包装器获取futurestd::future<RetType> ret = task->get_future();// 在{}内使用lock_guard锁定互斥量,并将task使用emplace插入至任务队列{std::lock_guard<std::mutex> cv_mt(cv_mt_);tasks_.emplace([task] { (*task)(); });}cv_lock_.notify_one();return ret;
}
commit 函数执行完后需要返回 std::packaged_task 对象的 future,以便获取任务执行结果,但是 future 的类型和可调用对象绑定,我们不知道任务执行的结果是什么,我们应该如何确定函数的返回类型?
我们通过 C++11 的尾置推导,std::future<decltype(std::forward<F>(f)(std::forward<Args>(args)...))>,decltype会根据根据表达式推断表达式的结果类型,我们用future存储这个类型,这个future就是返回值类型。
我们在对 thread、async、future源码进行分析的文章中详细介绍了完美转发原理,并且单独写了一篇文章对完美转发进行分析,可参考文章:
并发编程(1)——线程、thread源码解析 | 爱吃土豆的个人博客
并发编程(8)—— std::async、std::future 源码解析 | 爱吃土豆的个人博客
C++——完美转发(引用折叠+forward) | 爱吃土豆的个人博客
相关文章:
并发线程(21)——线程池
文章目录 二十一、day211. 线程池实现1.1 完整代码1.2 解释 二十一、day21 我们之前在学习std::future、std::async、std::promise相关的知识时,通过std::promise和packaged_task构建了一个可用的线程池,可参考文章:并发编程(6&a…...
基于32单片机的智能语音家居
一、主要功能介绍 以STM32F103C8T6单片机为控制核心,设计一款智能远程家电控制系统,该系统能实现如下功能: 1、可通过语音命令控制照明灯、空调、加热器、窗户及窗帘的开关; 2、可通过手机显示和控制照明灯、空调、窗户及窗帘的开…...
VScode怎么重启
原文链接:【vscode】vscode重新启动 键盘按下 Ctrl Shift p 打开命令行,如下图: 输入Reload Window,如下图:...
分析服务器 systemctl 启动gozero项目报错的解决方案
### 分析 systemctl start beisen.service 报错 在 Linux 系统中,systemctl 是管理系统和服务的主要工具。当我们尝试重启某个服务时,如果服务启动失败,systemctl 会输出错误信息,帮助我们诊断和解决问题。 本文将通过一个实际的…...
大模型LLM-Prompt-OPTIMAL
1 OPTIMAL OPTIMAL 具体每项内容解释如下: Objective Clarity(目标清晰):明确定义任务的最终目标和预期成果。 Purpose Definition(目的定义):阐述任务的目的和它的重要性。 Information Gat…...
3. 多线程(1) --- 创建线程,Thread类
文章目录 前言1. API2. 创建线程2.1. 继承 Thread类2.2. 实现 Runnable 接口2.3. 匿名内部类2.4. lambda2.5.其他方法 3. Thread类及其常见的方法和属性3.1. Thread 的常见构造方法3.2. Thread 的常见属性3.3. start() --- 启动一个线程3.4. 中断一个线程3.5. 等待线程3.6. 休眠…...
简单的jmeter数据请求学习
简单的jmeter数据请求学习 1.需求 我们的流程服务由原来的workflow-server调用wfms进行了优化,将wfms服务操作并入了workflow-server中,去除了原来的webservice服务调用形式,增加了并发处理,现在想测试模拟一下,在一…...
智能水文:ChatGPT等大语言模型如何提升水资源分析和模型优化的效率
大语言模型与水文水资源领域的融合具有多种具体应用,以下是一些主要的应用实例: 1、时间序列水文数据自动化处理及机器学习模型: ●自动分析流量或降雨量的异常值 ●参数估计,例如PIII型曲线的参数 ●自动分析降雨频率及重现期 ●…...
民宿酒店预订系统小程序+uniapp全开源+搭建教程
一.介绍 一.系统介绍 基于ThinkPHPuniappuView开发的多门店民宿酒店预订管理系统,快速部署属于自己民宿酒店的预订小程序,包含预订、退房、WIFI连接、吐槽、周边信息等功能。提供全部无加密源代码,支持私有化部署。 二.搭建环境 系统环境…...
计算机网络掩码、最小地址、最大地址计算、IP地址个数
一、必备知识 1.无分类地址IPV4地址网络前缀主机号 2.每个IPV4地址由32位二进制数组成 3. /15这个地址表示网络前缀有15位,那么主机号32-1517位。 4.IP地址的个数:2**n (n表示主机号的位数) 5.可用(可分配)IP地址个数&#x…...
Mac中配置vscode(第一期:python开发)
1、终端中安装 xcode-select --install #mac的终端中安装该开发工具 xcode-select -p #显示当前 Xcode 命令行工具的安装路径注意:xcode-select --install是在 macOS 上安装命令行开发工具(Command Line Tools)的关键命令。安装的主要组件包括:C/C 编…...
软件项目体系建设文档,项目开发实施运维,审计,安全体系建设,验收交付,售前资料(word原件)
软件系统实施标准化流程设计至关重要,因为它能确保开发、测试、部署及维护等各阶段高效有序进行。标准化流程能减少人为错误,提升代码质量和系统稳定性。同时,它促进了团队成员间的沟通与协作,确保项目按时交付。此外,…...
计算机网络--路由表的更新
一、方法 【计算机网络习题-RIP路由表更新-哔哩哔哩】 二、举个例子 例1 例2...
CDN防御如何保护我们的网络安全?
在当今数字化时代,网络安全成为了一个至关重要的议题。随着网络攻击的日益频繁和复杂化,企业和个人都面临着前所未有的安全威胁。内容分发网络(CDN)作为一种分布式网络架构,不仅能够提高网站的访问速度和用户体验&…...
matlab离线安装硬件支持包
MATLAB 硬件支持包离线安装 本文章提供matlab硬件支持包离线安装教程,因为我的matlab安装的某种原因(破解),不支持硬件支持包的安装,相信也有很多相同情况的朋友,所以记录一下我是如何离线安装的ÿ…...
使用virtualenv创建虚拟环境
下载 virtualenv pip install virtualenv 创建虚拟环境 先进入想要的目录 一般为 /envs virtualenv 文件名 --python解释器的版本 激活虚拟环境 .\虚拟项目的文件夹名称\Scripts\activate 退出虚拟环境 deactivate...
Java链表
链表(Linked List)是一种线性数据结构,它由一系列节点组成,每个节点包含两部分:一部分为用于储存数据元素,另部分是一种引用(指针),指向下一个节点。 这种结构允许动态地添加和删除元素,而不需要像数组那种大规模的数…...
Zero to JupyterHub with Kubernetes 下篇 - Jupyterhub on k8s
前言:纯个人记录使用。 搭建 Zero to JupyterHub with Kubernetes 上篇 - Kubernetes 离线二进制部署。搭建 Zero to JupyterHub with Kubernetes 中篇 - Kubernetes 常规使用记录。搭建 Zero to JupyterHub with Kubernetes 下篇 - Jupyterhub on k8s。 官方文档…...
解决 Tomcat 跨域问题 - Tomcat 配置静态文件和 Java Web 服务(Spring MVC Springboot)同时允许跨域
解决 Tomcat 跨域问题 - Tomcat 配置静态文件和 Java Web 服务(Spring MVC Springboot)同时允许跨域 Tomcat 配置允许跨域Web 项目配置允许跨域Tomcat 同时允许静态文件和 Web 服务跨域 偶尔遇到一个 Tomcat 部署项目跨域问题,因为已经处理…...
在C语言中使用伪终端与bash交互
了解伪终端概念: 伪终端(PTY)由一对设备组成:主设备(master)和从设备(slave)。数据写入主设备会出现在从设备,反之亦然。这允许一个进程通过主设备与另一个进程ÿ…...
浏览器访问 AWS ECS 上部署的 Docker 容器(监听 80 端口)
✅ 一、ECS 服务配置 Dockerfile 确保监听 80 端口 EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]或 EXPOSE 80 CMD ["python3", "-m", "http.server", "80"]任务定义(Task Definition&…...
在软件开发中正确使用MySQL日期时间类型的深度解析
在日常软件开发场景中,时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志,到供应链系统的物流节点时间戳,时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库,其日期时间类型的…...
RocketMQ延迟消息机制
两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数,对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后…...
css实现圆环展示百分比,根据值动态展示所占比例
代码如下 <view class""><view class"circle-chart"><view v-if"!!num" class"pie-item" :style"{background: conic-gradient(var(--one-color) 0%,#E9E6F1 ${num}%),}"></view><view v-else …...
CocosCreator 之 JavaScript/TypeScript和Java的相互交互
引擎版本: 3.8.1 语言: JavaScript/TypeScript、C、Java 环境:Window 参考:Java原生反射机制 您好,我是鹤九日! 回顾 在上篇文章中:CocosCreator Android项目接入UnityAds 广告SDK。 我们简单讲…...
【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)
🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...
土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等
🔍 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术,可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势,还能有效评价重大生态工程…...
Spring数据访问模块设计
前面我们已经完成了IoC和web模块的设计,聪明的码友立马就知道了,该到数据访问模块了,要不就这俩玩个6啊,查库势在必行,至此,它来了。 一、核心设计理念 1、痛点在哪 应用离不开数据(数据库、No…...
OPENCV形态学基础之二腐蚀
一.腐蚀的原理 (图1) 数学表达式:dst(x,y) erode(src(x,y)) min(x,y)src(xx,yy) 腐蚀也是图像形态学的基本功能之一,腐蚀跟膨胀属于反向操作,膨胀是把图像图像变大,而腐蚀就是把图像变小。腐蚀后的图像变小变暗淡。 腐蚀…...
视觉slam十四讲实践部分记录——ch2、ch3
ch2 一、使用g++编译.cpp为可执行文件并运行(P30) g++ helloSLAM.cpp ./a.out运行 二、使用cmake编译 mkdir build cd build cmake .. makeCMakeCache.txt 文件仍然指向旧的目录。这表明在源代码目录中可能还存在旧的 CMakeCache.txt 文件,或者在构建过程中仍然引用了旧的路…...
