并发线程(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)。数据写入主设备会出现在从设备,反之亦然。这允许一个进程通过主设备与另一个进程ÿ…...
linux之kylin系统nginx的安装
一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源(HTML/CSS/图片等),响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址,提高安全性 3.负载均衡服务器 支持多种策略分发流量…...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(二)
HoST框架核心实现方法详解 - 论文深度解读(第二部分) 《Learning Humanoid Standing-up Control across Diverse Postures》 系列文章: 论文深度解读 + 算法与代码分析(二) 作者机构: 上海AI Lab, 上海交通大学, 香港大学, 浙江大学, 香港中文大学 论文主题: 人形机器人…...
基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...
听写流程自动化实践,轻量级教育辅助
随着智能教育工具的发展,越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式,也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建,…...
DingDing机器人群消息推送
文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人,点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置,详见说明文档 成功后,记录Webhook 2 API文档说明 点击设置说明 查看自…...
LabVIEW双光子成像系统技术
双光子成像技术的核心特性 双光子成像通过双低能量光子协同激发机制,展现出显著的技术优势: 深层组织穿透能力:适用于活体组织深度成像 高分辨率观测性能:满足微观结构的精细研究需求 低光毒性特点:减少对样本的损伤…...
五子棋测试用例
一.项目背景 1.1 项目简介 传统棋类文化的推广 五子棋是一种古老的棋类游戏,有着深厚的文化底蕴。通过将五子棋制作成网页游戏,可以让更多的人了解和接触到这一传统棋类文化。无论是国内还是国外的玩家,都可以通过网页五子棋感受到东方棋类…...
【深度学习新浪潮】什么是credit assignment problem?
Credit Assignment Problem(信用分配问题) 是机器学习,尤其是强化学习(RL)中的核心挑战之一,指的是如何将最终的奖励或惩罚准确地分配给导致该结果的各个中间动作或决策。在序列决策任务中,智能体执行一系列动作后获得一个最终奖励,但每个动作对最终结果的贡献程度往往…...
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
文章目录 一、开启慢查询日志,定位耗时SQL1.1 查看慢查询日志是否开启1.2 临时开启慢查询日志1.3 永久开启慢查询日志1.4 分析慢查询日志 二、使用EXPLAIN分析SQL执行计划2.1 EXPLAIN的基本使用2.2 EXPLAIN分析案例2.3 根据EXPLAIN结果优化SQL 三、使用SHOW PROFILE…...
从零开始了解数据采集(二十八)——制造业数字孪生
近年来,我国的工业领域正经历一场前所未有的数字化变革,从“双碳目标”到工业互联网平台的推广,国家政策和市场需求共同推动了制造业的升级。在这场变革中,数字孪生技术成为备受关注的关键工具,它不仅让企业“看见”设…...
