Linux从0到1——进程池

Linux从0到1——进程池

  • 1. 进程池的概念
  • 2. 进程池实现思路
  • 3. 进程池的代码实现
    • 3.1 创建管道,创建子进程
    • 3.2 封装任务
    • 3.3 Work接口
    • 3.4 发送任务
    • 3.5 回收资源,关闭管道(重点)
    • 3.6 改造CreatChannels接口
  • 4. 完整代码


1. 进程池的概念


1. 池化技术:

  • 在古代,有一种建筑叫粮仓,用来储存粮食。人们一般会先把大量的粮食囤积在粮仓中,等到需要用粮食时,再从粮仓中一点一点的拿。相当于是先把粮食提前准备好,你需要的时候直接去拿就行了,不然的话每次用粮食前还要先去田里收割粮食。

2. 内存池:

  • 计算机中有很多的设计理念,都用到了池化技术。如内存池,当一个进程需要申请空间时,OS会一次性开辟好大量的空间,进程直接去使用这些空间即可。而不是每一次进程申请空间时,操作系统都要做开辟空间的动作,这样太低效了。

3. 进程池:

  • 如果我们每启动一个任务,就创建一个进程来完成这个任务,这样未免有些低效。我们可以将多个进程一次性开辟好,等任务来的时候,直接丢给开辟好的进程即可,省去了多次创建进程的时间开销。

2. 进程池实现思路


在这里插入图片描述

  • 一次性开辟多个管道和多个子进程,让每一个管道和子进程一一对应。

  • 父进程每次向管道中送入4字节int类型的数据,表示需要执行任务的任务码code,一旦管道中被放入了数据,意味着这个管道对应的子进程被激活,开始执行任务码对应的任务。


3. 进程池的代码实现


3.1 创建管道,创建子进程


1. 描述管道:

  • 为了管理我们创建的管道,需要为其创建对应的结构体来描述它,然后用一定的数据结构组织起来。
const int num = 5; // 最大管道数
int number = 0;    // 管道编号class channel
{
public:channel(int fd, pid_t id): ctrlfd(fd), workerid(id){name = "channel-" + std::to_string(++number);}public:int ctrlfd;         // 管道写端pid_t workerid;     // 对应的子进程pidstd::string name;   // 管道名
};
  • 结构体中,需要记录管道的写端,以便将来父进程关闭。
  • 还需要记录对应的子进程pid,以便子进程退出后,wait处理子进程僵尸状态。

2. 创建管道,创建进程:

  • 介绍一下C++中规范的传参形式:
    • 输入型参数:const &
    • 输出型参数:*
    • 输入输出型参数:&
// 传参形式:
// 1. 输入参数:const &
// 2. 输出参数:*
// 3. 输入输出参数:&void CreatChannels(std::vector<channel> *c)
{// bug versionfor (int i = 0; i < num; i++){// 1. 定义并创建管道int pipefd[2];int n = pipe(pipefd);assert(n == 0);(void)n;// 2. 创建进程pid_t id = fork();assert(id != -1);// 3. 构建单向通信信道if (id == 0){// childclose(pipefd[1]); // 关闭写端// TODOdup2(pipefd[0], 0); // 重定向Work();exit(0);    // 会自动关闭自己打开的所有fd}// fatherclose(pipefd[0]); // 关闭读端c->push_back(channel(pipefd[1], id));}
}int main()
{std::vector<channel> channels;// 创建信道,创建进程CreatChannels(&channels);// ...return 0;
}
  • Work函数为将来子进程执行任务时要完成的模块。重定向操作执行后,每一个子进程中,fd为0的位置就指向读端。
  • 每建立一条管道,就将它放入数组中。这样,对管道的管理就变为了对数组的管理。

上面这种创建管道和进程的方式有一个很深层次的bug,我们到后面说。


3.2 封装任务


Task.hpp头文件:

#pragma once#include <iostream>
#include <functional>
#include <vector>
#include <ctime>
#include <unistd.h>typedef std::function<void()> task_t;// 三个任务
void Download()
{std::cout << "我是一个下载任务" << " 处理者: " << getpid() << std::endl;
}void PrintLog()
{std::cout << "我是一个打印日志的任务" << " 处理者: " << getpid() << std::endl;
}void PushVideoStream()
{std::cout << "我是一个推送视频流的任务" << " 处理者: " << getpid() << std::endl;
}class Init
{
public:// 任务码const static int g_download_code = 0;const static int g_printlog_code = 1;const static int g_push_videostream_code = 2;// 任务集合std::vector<task_t> tasks;
public:Init(){tasks.push_back(Download);tasks.push_back(PrintLog);tasks.push_back(PushVideoStream);srand(time(nullptr) ^ getpid());    // ^getpid(),让数据更有随机性}// 检查任务码是否安全bool CheckSafe(int code){if (code >= 0 && code < tasks.size()) return true;else return false;}// 执行任务void RunTask(int code){tasks[code]();}// 选择任务int SelectTask(){// 返回随机任务码return rand() % tasks.size();}// 返回任务码对应的任务std::string ToDesc(const int &code){switch(code){case g_download_code:return "Download";case g_printlog_code:return "PrintLog";case g_push_videostream_code:return "PushVideoStream";default:return "Unknow";}return "";}
};Init init;  // 定义对象
  • 定义了三条任务,即这三条任务对应的任务码。
  • 定义了一个Init类来封装这些任务,以及提供对应的接口。
  • 这里我们采取随机选择任务的设计,实际应用中会有所不同。三个任务也只是象征性的模拟一下,并不是具体的下载,打印日志,推送视频流任务。

3.3 Work接口


不断从管道中读取任务码,执行对应的任务。直到写端关闭。

void Work()
{while (true){int code = 0;   // 任务码ssize_t n = read(0, &code, sizeof(code));if (n == sizeof(code)){if (!init.CheckSafe(code))continue;init.RunTask(code);}else if (n == 0){// n == 0 写端退出break;}else{// do nothing}}std::cout << "child quit" << std::endl;
}

3.4 发送任务


  • 设计了标签g_always_loop来控制是否一直发送任务。true为一直发送,false为不是一直发送。传false要配合参数num,标明发送次数。
  • 轮巡式的向管道中发送信息。
const bool g_always_loop = true;    // 是否一直执行void SendCommand(const std::vector<channel> &channels, bool flag, int num = -1)
{int pos = 0;while (true){// 1. 选择任务int command = init.SelectTask();// 2. 选择信道(进程)const auto &c = channels[pos++];pos %= channels.size();// debugstd::cout << "send command " << init.ToDesc(command) << "[" << command << "]" << " in" << c.name << "worker is: " << c.workerid << std::endl;// 3. 发送任务write(c.ctrlfd, &command, sizeof(command));// 4. 判断是否要退出if (!flag){num--;if (num <= 0) break;}sleep(1);}std::cout << "SendCommand done..." << std::endl;
}int main()
{std::vector<channel> channels;// 创建信道,创建进程CreatChannels(&channels);// 开始发送任务SendCommand(channels, !g_always_loop, 10);// ...return 0;
}

3.5 回收资源,关闭管道(重点)


1. 版本1:

  • 这种版本为,先将写端全部关闭,再回收子进程,自动关闭读端,循环两次。该版本不会出bug
  • 原理是,写端关闭后,Work模块中read会读到0,子进程就会执行到exit退出。
void ReleaseChannels(const std::vector<channel> &channels)
{// version1for (const auto &c : channels){close(c.ctrlfd);}for (const auto &c : channels){pid_t rid = waitpid(c.workerid, nullptr, 0);if (rid == c.workerid){std::cout << "wait child: " << c.workerid << " success" << std::endl;}}
}int main()
{std::vector<channel> channels;// 创建信道,创建进程CreatChannels(&channels);// 开始发送任务SendCommand(channels, !g_always_loop, 10);// 回收资源,想让子进程退出,并且释放管道资源,只要关闭写端即可(写端关闭后,子进程自动退出)ReleaseChannels(channels);return 0;
}

2. 会出现bug的版本:

  • 关闭一个写端,就关闭对应的读端,循环一次。
  • 这种版本会出现bug,现象就是会在发送任务完成时卡死。
void ReleaseChannels(const std::vector<channel> &channels)
{// bug versionfor (const auto &c : channels){close(c.ctrlfd);pid_t rid = waitpid(c.workerid, nullptr, 0);if (rid == c.workerid){std::cout << "wait child: " << c.workerid << " success" << std::endl;}}
}

在这里插入图片描述

3. 分析bug出现的原因:

在这里插入图片描述

  • 这个bug要追溯到创建管道的模块。首先,如果以我们之前的方式创建管道和进程,那么在创建第二个管道时,子进程拷贝父进程struct files_struct结构体,将上一个管道的写端也拷贝下来了。此时,第一个管道就会有两个写端。
  • 以此类推,在创建第三个管道时,子进程又将父进程的struct files_struct结构体继承下来了。第一个管道就会有三个写端,第二个管道会有两个写端。
  • 此时如果从第一个管道开始关闭管道,回收子进程,就会出现,第一个管道只关闭了一个写端,read无法读取到0,子进程在read处阻塞的情况。
  • 版本1之所以不会出现bug,是因为,最后一个管道只有一个写端,将父进程对应的写端一次性全部关闭后,最后一个进程就没有写端了,最后一个子进程就会退出。最后一个子进程退出了,倒数第二个管道也就没有写端了,倒数第二个子进程也要跟着退出。从后向前,管道和子进程资源依次释放。

2. 版本2:

  • 根据上面分析的bug出现原因,我们只需要从后向前关闭管道即可。
void ReleaseChannels(const std::vector<channel> &channels)
{// version2int num = channels.size() - 1;for (; num >= 0; num--){close(channels[num].ctrlfd);pid_t rid = waitpid(channels[num].workerid, nullptr, 0);if (rid == channels[num].workerid){std::cout << "wait child: " << channels[num].workerid << " success" << std::endl;}}
}

在这里插入图片描述


3.6 改造CreatChannels接口


通过3.5的讲解,我们是可以从后向前关闭管道,来解决卡死的问题。但是,这毕竟不是问题的本质,我们期望的管道是单向通信的,也就是只有一个写端和一个读端,出现一个管道多个写端的情况,不是我们想看到的。

所以,我们可以在创建管道时,提前记录一下子进程都从父进程继承下来了哪些写端,然后让子进程将这些写端关闭。

void CreatChannels(std::vector<channel> *c)
{std::vector<int> old;for (int i = 0; i < num; i++){// 1. 定义并创建管道int pipefd[2];int n = pipe(pipefd);assert(n == 0);(void)n;// 2. 创建进程pid_t id = fork();assert(id != -1);// 3. 构建单向通信信道if (id == 0){// child// 关闭从父进程继承下来的写端if (!old.empty()){for (auto fd : old){close(fd);}// debugPrintFd(old);}close(pipefd[1]); // 关闭写端// TODOdup2(pipefd[0], 0); // 重定向Work();exit(0);    // 会自动关闭自己打开的所有fd}// fatherclose(pipefd[0]); // 关闭读端c->push_back(channel(pipefd[1], id));old.push_back(pipefd[1]);}
}
  • 这样一来,问题就解决了,回收资源时使用3.5中的bug版本也没有问题。可以说,那个bug版本才是逻辑最恰当的版本。

4. 完整代码


1. Task.hpp头文件:

#pragma once#include <iostream>
#include <functional>
#include <vector>
#include <ctime>
#include <unistd.h>typedef std::function<void()> task_t;void Download()
{std::cout << "我是一个下载任务" << " 处理者: " << getpid() << std::endl;
}void PrintLog()
{std::cout << "我是一个打印日志的任务" << " 处理者: " << getpid() << std::endl;
}void PushVideoStream()
{std::cout << "我是一个推送视频流的任务" << " 处理者: " << getpid() << std::endl;
}class Init
{
public:// 任务码const static int g_download_code = 0;const static int g_printlog_code = 1;const static int g_push_videostream_code = 2;// 任务集合std::vector<task_t> tasks;
public:Init(){tasks.push_back(Download);tasks.push_back(PrintLog);tasks.push_back(PushVideoStream);srand(time(nullptr) ^ getpid());    // ^getpid(),让数据更有随机性}// 检查任务码是否安全bool CheckSafe(int code){if (code >= 0 && code < tasks.size()) return true;else return false;}// 执行任务void RunTask(int code){tasks[code]();}int SelectTask(){// 返回随机任务码return rand() % tasks.size();}// 返回任务码对应的具体任务std::string ToDesc(const int &code){switch(code){case g_download_code:return "Download";case g_printlog_code:return "PrintLog";case g_push_videostream_code:return "PushVideoStream";default:return "Unknow";}return "";}
};Init init;  // 定义对象

2. ProcessPool.cc文件:

#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include "Task.hpp"const int num = 5; // 最大管道数
int number = 0;    // 管道编号class channel
{
public:channel(int fd, pid_t id): ctrlfd(fd), workerid(id){name = "channel-" + std::to_string(++number);}public:int ctrlfd;         // 管道写端pid_t workerid;     // 对应的子进程pidstd::string name;   // 管道名
};void Work()
{while (true){int code = 0;   // 任务码ssize_t n = read(0, &code, sizeof(code));if (n == sizeof(code)){if (!init.CheckSafe(code))continue;init.RunTask(code);}else if (n == 0){// n == 0 写端退出break;}else{// do nothing}}std::cout << "child quit" << std::endl;
}// 打印都关闭了哪些从父进程继承下来的写端
void PrintFd(const std::vector<int> &fds)
{std::cout << getpid() << " close: ";for (auto fd : fds){std::cout << fd << " ";}std::cout << std::endl;
}// 传参形式:
// 1. 输入参数:const &
// 2. 输出参数:*
// 3. 输入输出参数:&void CreatChannels(std::vector<channel> *c)
{std::vector<int> old;for (int i = 0; i < num; i++){// 1. 定义并创建管道int pipefd[2];int n = pipe(pipefd);assert(n == 0);(void)n;// 2. 创建进程pid_t id = fork();assert(id != -1);// 3. 构建单向通信信道if (id == 0){// child// 关闭从父进程继承下来的写端if (!old.empty()){for (auto fd : old){close(fd);}// debugPrintFd(old);}close(pipefd[1]); // 关闭写端// TODOdup2(pipefd[0], 0); // 重定向Work();exit(0);    // 会自动关闭自己打开的所有fd}// fatherclose(pipefd[0]); // 关闭读端c->push_back(channel(pipefd[1], id));old.push_back(pipefd[1]);}// bug version// for (int i = 0; i < num; i++)// {//     // 1. 定义并创建管道//     int pipefd[2];//     int n = pipe(pipefd);//     assert(n == 0);//     (void)n;//     // 2. 创建进程//     pid_t id = fork();//     assert(id != -1);//     // 3. 构建单向通信信道//     if (id == 0)//     {//         // child//         close(pipefd[1]); // 关闭写端//         // TODO//         dup2(pipefd[0], 0); // 重定向//         Work();//         exit(0);    // 会自动关闭自己打开的所有fd//     }//     // father//     close(pipefd[0]); // 关闭读端//     c->push_back(channel(pipefd[1], id));// }
}void PrintfDebug(const std::vector<channel> &c)
{for (const auto &channel : c){std::cout << channel.name << ", " << channel.ctrlfd << ", " << channel.workerid << std::endl;}
}const bool g_always_loop = true;    // 是否一直执行void SendCommand(const std::vector<channel> &channels, bool flag, int num = -1)
{int pos = 0;while (true){// 1. 选择任务int command = init.SelectTask();// 2. 选择信道(进程)const auto &c = channels[pos++];pos %= channels.size();// debugstd::cout << "send command " << init.ToDesc(command) << "[" << command << "]" << " in" << c.name << "worker is: " << c.workerid << std::endl;// 3. 发送任务write(c.ctrlfd, &command, sizeof(command));// 4. 判断是否要退出if (!flag){num--;if (num <= 0) break;}sleep(1);}std::cout << "SendCommand done..." << std::endl;
}void ReleaseChannels(const std::vector<channel> &channels)
{// version2// int num = channels.size() - 1;// for (; num >= 0; num--)// {//     close(channels[num].ctrlfd);//     pid_t rid = waitpid(channels[num].workerid, nullptr, 0);//     if (rid == channels[num].workerid)//     {//         std::cout << "wait child: " << channels[num].workerid << " success" << std::endl;//     }// }// version1// for (const auto &c : channels)// {//     close(c.ctrlfd);// }// for (const auto &c : channels)// {//     pid_t rid = waitpid(c.workerid, nullptr, 0);//     if (rid == c.workerid)//     {//         std::cout << "wait child: " << c.workerid << " success" << std::endl;//     }// }// bug versionfor (const auto &c : channels){close(c.ctrlfd);pid_t rid = waitpid(c.workerid, nullptr, 0);if (rid == c.workerid){std::cout << "wait child: " << c.workerid << " success" << std::endl;}}
}int main()
{std::vector<channel> channels;// 创建信道,创建进程CreatChannels(&channels);// 开始发送任务SendCommand(channels, !g_always_loop, 10);// PrintfDebug(channels);// sleep(10);// 回收资源,想让子进程退出,并且释放管道资源,只要关闭写端即可(写端关闭后,子进程自动退出)ReleaseChannels(channels);return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/399296.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ECMAScript6模板字面量:反引号、${}占位符的使用

ECMAScript 6 中引入了模板字面量&#xff0c;主要通过多行字符串和字符串占位符对字符串进行增强操作。如下&#xff1a; //使用ECMAScript6模板字面量拼接字符串&#xff0c;例如&#xff1a;2024年8月12日 15:38:28 星期一 let dateRet ${Year}年${Month}月${Dates}日 ${H…

lvs、集群

1.集群和分布式 当多个用户当用户访问一个服务器时&#xff0c;服务器server1可能就会崩&#xff0c;假如这时候我们新加一个服务器server2来缓解server1的压力&#xff0c;那么就需要一个调度器lvs来分配&#xff0c;所以现在就是用户的访问就需要通过调度器之后到达服务器&a…

简述MYSQL聚簇索引、二级索引、索引下推

一丶聚簇索引 InnoDB的索引分为两种&#xff1a; 聚簇索引&#xff1a;一般创建表时的主键就会被mysql作为聚簇索引&#xff0c;如果没有主键则选择非空唯一索引作为聚簇索引&#xff0c;都没有则隐式创建一个索引作为聚簇索引&#xff1b;辅助索引&#xff1a;也就是非聚簇索…

KillWxapkg 自动化反编译微信小程序,小程序安全评估工具,发现小程序安全问题,自动解密,解包,可还原工程目录,支持修改Hook,小程序

纯Golang实现&#xff0c;一个用于自动化反编译微信小程序的工具&#xff0c;小程序安全利器&#xff0c;自动解密&#xff0c;解包&#xff0c;可还原工程目录&#xff0c;支持微信开发者工具运行 由于采用了UPX压缩的软件体积&#xff0c;工具运行时可能会出现错误报告&…

在等保测评中,如何平衡技术风险和非技术风险的评估?

在等保测评中平衡技术风险和非技术风险的评估&#xff0c;需要一个综合的方法来确保所有相关的风险都得到适当的考虑。以下是一些关键步骤&#xff1a; 1. 全面风险识别&#xff1a;首先识别所有可能影响组织的风险&#xff0c;包括技术风险&#xff08;如系统漏洞、恶意软件&…

企业大模型落地从0到0.1

现在人工智能里的“大明星”——大模型&#xff0c;正在悄悄改变各行各业。这就像给企业装上了一颗聪明的大脑&#xff0c;能帮助解决各种棘手问题&#xff0c;提升工作效率。今天&#xff0c;我们就来分析下企业如何一步一步让这个“大脑”在自家地盘里真正派上用场&#xff0…

九、OpenCVSharp 中的图像形态学操作

文章目录 简介一、腐蚀1. 腐蚀的原理和数学定义2. 结构元素的形状和大小选择3. 腐蚀操作的代码实现和效果展示二、膨胀1. 膨胀的概念和作用2. 与腐蚀的对比和组合使用(如开运算、闭运算)三、开运算1. 开运算的定义和用途(去除小的明亮区域)2. 开运算在去除噪声和分离物体方…

数据结构--树与二叉树

数据结构分类 集合 线性结构(一对一) 树形结构(一对多) 图结构(多对多) 数据结构三要素 1、逻辑结构 2、数据的运算 3、存储结构&#xff08;物理结构&#xff09; 树的概念 树的分类 满二叉树和完全二叉树 二叉排序树 平衡二叉树 二叉树分类总结 二叉树的存储结构 …

Element-UI自学实践

概述 Element-UI 是由饿了么前端团队推出的一款基于 Vue.js 2.0 的桌面端 UI 组件库。它为开发者提供了一套完整、易用、美观的组件解决方案&#xff0c;极大地提升了前端开发的效率和质量。本文为自学实践记录&#xff0c;详细内容见 &#x1f4da; ElementUI官网 1. 基础组…

Linux os下借助Qt+libvlc是实现多路拉取摄像头rtsp数据流并实时显示

前言 应客户方的一个实际项目需求&#xff0c;需要在Linux操作系统下拉取多路摄像头的RTSP数据流并实时显示。 该项目的硬件平台基于飞腾2000四核处理器与景嘉微显卡&#xff0c;搭载了Kylin V10操作系统。 当前景嘉微GPU最多支持同时连接16路摄像头&#xff0c;拉取1920x108…

在等保测评中,如何平衡资产识别的全面性和准确性,避免过度关注某些资产而忽视其他潜在风险?

在等保测评中平衡资产识别的全面性和准确性&#xff0c;避免过度关注某些资产而忽视其他潜在风险&#xff0c;可以通过以下策略实现&#xff1a; 1. 全面审计&#xff1a;确保进行一次全面的审计&#xff0c;包括所有类型的资产&#xff0c;避免遗漏任何关键组件。 2. 风险导…

上海亚商投顾:三大指数小幅调整,两市成交不足5000亿

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 大指数昨日窄幅震荡&#xff0c;临近尾盘小幅下挫。环保板块开盘大涨&#xff0c;永清环保、清研环境、中兰环…

OpenCV图像滤波(10)Laplacian函数的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 功能描述 计算图像的拉普拉斯值。 该函数通过使用 Sobel 运算符计算出的 x 和 y 的二阶导数之和来计算源图像的拉普拉斯值&#xff1a; dst Δ src ∂…

LeetCode刷题笔记第191题:位1的个数

LeetCode刷题笔记第191题&#xff1a;位1的个数 题目&#xff1a; 想法&#xff1a; 通过位运算判断二级制形式中有多少个1&#xff0c;代码及解释如下&#xff1a; class Solution:def hammingWeight(self, n: int) -> int:return sum(1 for i in range(32) if n & …

Latex或者word里面mathtype类型的数学公式如何变成mathematica里面的形式

详细步骤如下&#xff1a; 第一步&#xff1a;Latex里面的公式复制粘贴到word里面&#xff0c;转变成mathtype类型的数学公式&#xff08;若已经是word里面mathtype类型的数学公式&#xff0c;这一步可以省略&#xff09;&#xff0c;如下&#xff1a; 第二步&#xff1a;将ma…

数字孪生赋能智慧城市大脑智建设方案(可编辑65页PPT)

引言&#xff1a;随着科技的飞速发展&#xff0c;智慧城市的建设已成为全球城市发展的新趋势。数字孪生技术作为其中的关键技术之一&#xff0c;正逐步赋能智慧城市大脑的建设&#xff0c;推动城市治理从数字化向智能化、智慧化转型升级。本方案旨在简要介绍数字孪生赋能智慧城…

Mapreduce_csv_averageCSV文件计算平均值

csv文件求某个平均数据 查询每个部门的平均工资&#xff0c;最后输出 数据处理过程 employee_noheader.csv&#xff08;没做关于首行的处理&#xff0c;运行时请自行删除&#xff09; EmployeeID,EmployeeName,DepartmentID,Salary 1,ZhangSan,101,5000 2,LiSi,102,6000…

VisionPro二次开发学习笔记13-使用CogToolBlock进行图像交互

该程序演示了如何使用CogToolBlock进行图像交互. 从vpp文件中加载一个ToolBlock。 用户可以通过应用程序窗体上的数字增减控件修改ToolBlock输入端子的值。 用户还可以从coins.idb或采集FIFO中选择图像。 “运行一次”按钮执行以下操作&#xff1a; 获取下一个图像或读取下一…

Android Studio 连接手机进行调试

总所周知&#xff0c;Android Studio里的虚拟手机下载后又大又难用。不如直接连手机用。本篇文章主要内容为Android Studio怎么连接手机进行程序调试。 1. 在AndroidSDK中下载google USB Driver: 2. 连接手机&#xff1a; 进入电脑设备管理器界面。并点开便携设备&#xff0c…

Java生成图形验证码

1、加依赖 <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.16</version></dependency> 2、写接口&#xff0c;这块不需要登录成功才能操作的&#xff0c;所以写controller就行了…