Linux--构建进程池

目录

 1.进程池

1.1.我们先完成第一步,创建子进程和信道

1.2. 通过channel控制,发送任务

1.3回收管道和子进程

1.4进行测试

1.5完整代码


 1.进程池

进程池其产生原因主要是为了优化大量任务需要多进程完成时频繁创建和删除进程所带来的资源消耗,以及更好的实现多个进程间的协同。以下是关于进程池的一些关键点:

  1. 组成:进程池技术主要由两部分组成:资源进程和管理进程。资源进程是预先创建好的空闲进程,等待管理进程分发任务(maste向哪个管道写入,就是唤醒哪一个子进程处理任务)。管理进程则负责创建这些资源进程,将工作分配给空闲的资源进程处理,并在工作完成后回收这些资源进程。(父进程要进行后端任务的负载均衡)
  2. 管理:管理进程如何有效地管理资源进程是关键。这涉及到分配任务给资源进程、回收空闲资源进程等操作。管理进程和资源进程之间需要进行交互,这种交互可以通过管道(需要子进程执行一个任务我们就可以派发一个管道,连接一个子进程,执行任务)
  3. 使用场景:当我们需要并行的处理大规模任务时,比如服务器处理大量客户端的任务,进程池是一个很好的选择。它可以避免频繁创建和销毁进程的开销,提高应用的响应速度。
  4. 优化:使用进程池可以避免动态创建和销毁进程的开销,提高应用的性能和稳定性。此外,进程池还可以根据任务的执行情况尽量减少创建的进程数量,最多创建指定个数的进程。

代码实现:

        实现进程池首先我们需要提前准备好一批数量的进程,当一个任务队列被提交到线程池,每个工作进程都会不断从任务队列中取出任务并执行。


1.1.我们先完成第一步,创建子进程和信道

        

        Channel类是来管理一个子进程和与之相关联的管道写端,这个Channel类就是信道。用于一个需要管理多个子进程和它们各自管道写端的程序中。例如,你可能有一个父进程,它创建多个子进程,每个子进程都通过管道与父进程通信。在这种情况下,你可以为每个子进程和它的管道写端创建一个Channel对象,并使用这些对象来管理它们。

class Channel
{
public:Channel(int wfd, pid_t id, const std::string &name): _wfd(wfd), _subprocessid(id), _name(name){}int GetWfd() { return _wfd; }pid_t GetProcessId() { return _subprocessid; }std::string GetName() { return _name; }void CloseChannel(){close(_wfd);}void Wait(){pid_t rid = waitpid(_subprocessid, nullptr, 0);if (rid > 0){std::cout << "wait " << rid << " success" << std::endl;}}~Channel(){}private:int _wfd;//写端fdpid_t _subprocessid;//子进程id'std::string _name;//一个信道的名称
};

        下面函数的主要逻辑是创建多个管道和相应的子进程,每个子进程都将从它自己的管道中读取数据,而父进程则保留管道的写端以便后续向子进程发送数据。同时,这个函数也维护了一个 Channel 对象的向量(std::vector<Channel>),其中每个 Channel 对象代表一个与特定子进程和管道写端相关联的通道。

        task_t是一个函数指针类型,是为了泛型,将来可以传任意不同的任务交给子进程做。

可以理解为回调函数 task 的使用降低了任务与子进程之间的耦合度。在传统的编程模型中,你可能会为每个子进程编写特定的代码块,这些代码块直接嵌入在创建子进程的函数中。这样做会导致代码的高耦合性,因为每个子进程的行为都紧密地绑定在创建它们的函数中。(关于task的应用见下文)

        dup2(pipefd[0], 0)将管道的读端,重定向到标准输入。意思就是管道的读端变成了标准输入,无需修改代码以处理文件描述符,因为以后任意的子进程去读取任务,都是从标准输入去读取的。

        在子进程中,它关闭了管道的写端,将读端重定向到标准输入,执行给定的回调函数 task,然后关闭读端并退出。

void CreateChannelAndSub(int num, std::vector<Channel> *channels, task_t task)
{// BUG? --> fix bugfor (int i = 0; i < num; i++){// 1. 创建管道int pipefd[2] = {0};int n = pipe(pipefd);if (n < 0)exit(1);// 2. 创建子进程pid_t id = fork();if (id == 0){if (!channels->empty()){// 第二次之后,开始创建的管道for(auto &channel : *channels) channel.CloseChannel();}// child - readclose(pipefd[1]);dup2(pipefd[0], 0); // 将管道的读端,重定向到标准输入task();close(pipefd[0]);exit(0);}// 3.构建一个channel名称std::string channel_name = "Channel-" + std::to_string(i);// 父进程close(pipefd[0]);// a. 子进程的pid b. 父进程关心的管道的w端channels->push_back(Channel(pipefd[1], id, channel_name));}
}

提供一个任务文件,接下来让子进程执行任务:

#pragma once#include <iostream>
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>#define TaskNum 3typedef void (*task_t)(); // task_t 函数指针类型void Print()
{std::cout << "I am print task" << std::endl;
}
void DownLoad()
{std::cout << "I am a download task" << std::endl;
}
void Flush()
{std::cout << "I am a flush task" << std::endl;
}task_t tasks[TaskNum];void LoadTask()
{srand(time(nullptr) ^ getpid() ^ 17777);tasks[0] = Print;tasks[1] = DownLoad;tasks[2] = Flush;
}void ExcuteTask(int number)
{if (number < 0 || number > 2)return;tasks[number]();
}int SelectTask()
{return rand() % TaskNum;
}void work()
{while (true){int command = 0;int n = read(0, &command, sizeof(command));if (n == sizeof(int)){std::cout << "pid is : " << getpid() << " handler task" << std::endl;ExcuteTask(command);}else if (n == 0){std::cout << "sub process : " << getpid() << " quit" << std::endl;break;}}
}
  • 初始化阶段:LoadTask()函数被调用,设置tasks函数指针数组以包含所有可用的任务函数,通过索引调用三个具体的任务函数:Print()DownLoad(), 和 Flush()。
  • 任务选择阶段:SelectTask()函数在需要时用于生成一个随机任务索引。
  • 任务执行阶段:在子进程中,work()函数不断监听标准输入,等待一个任务索引。一旦接收到索引,就调用ExcuteTask()来执行相应的任务。

子进程退出:当子进程从标准输入读取到0时,它知道应该退出循环并结束。


1.2. 通过channel控制,发送任务

int NextChannel(int channelnum)
{static int next = 0;int channel = next;next++;next %= channelnum;return channel;
}
void SendTaskCommand(Channel &channel, int taskcommand)
{write(channel.GetWfd(), &taskcommand, sizeof(taskcommand));
}
void ctrlProcessOnce(std::vector<Channel> &channels)
{sleep(1);// a. 选择一个任务int taskcommand = SelectTask();// b. 选择一个信道和进程int channel_index = NextChannel(channels.size());// c. 发送任务SendTaskCommand(channels[channel_index], taskcommand);std::cout << std::endl;std::cout << "taskcommand: " << taskcommand << " channel: "<< channels[channel_index].GetName() << " sub process: " << channels[channel_index].GetProcessId() << std::endl;
}
void ctrlProcess(std::vector<Channel> &channels, int times = -1)
{if (times > 0){while (times--){ctrlProcessOnce(channels);}}else{while (true){ctrlProcessOnce(channels);}}
}
  1.   NextChannel 函数是一个轮询选择器,用于在给定的通道数量(channelnum)中循环选择一个通道索引。它使用了一个静态变量 next 来跟踪上一次选择的索引,并在每次调用时递增这个索引。
  2.  SendTaskCommand 函数用于向指定的 Channel 对象(代表一个与子进程的通信通道)发送一个任务命令(taskcommand。它通过调用 Channel 对象的 GetWfd 方法获取管道的写端文件描述符,然后使用 write 系统调用来将任务命令写入管道。这样,子进程就可以从管道中读取这个命令并执行相应的任务。
  3.  ctrlProcessOnce 函数模拟了控制进程的一次操作。调用 NextChannel 函数来选择一个通道索引,并通过该索引从 channels 向量中获取一个 Channel 对象。然后,它调用 SendTaskCommand 函数向选定的通道发送任务命令。最后,它打印出相关的信息,包括任务命令、通道名称和子进程的进程ID,以便进行调试和监控。               
  4. ctrlProcess 函数是控制进程的主循环。它接受一个 channels 向量和一个可选的 times 参数。如果 times 大于0,则控制进程会循环执行 ctrlProcessOnce 函数 times 次;如果 times 小于或等于0(默认为-1),则控制进程会无限循环地执行 ctrlProcessOnce 函数

1.3回收管道和子进程

class Channel
{
public:Channel(int wfd, pid_t id, const std::string &name): _wfd(wfd), _subprocessid(id), _name(name){}int GetWfd() { return _wfd; }pid_t GetProcessId() { return _subprocessid; }std::string GetName() { return _name; }void CloseChannel(){close(_wfd);}void Wait(){pid_t rid = waitpid(_subprocessid, nullptr, 0);if (rid > 0){std::cout << "wait " << rid << " success" << std::endl;}}~Channel(){}private:int _wfd;//写端fdpid_t _subprocessid;//子进程id'std::string _name;//一个信道的名称
};void CleanUpChannel(std::vector<Channel> &channels)
{for (auto &channel : channels){channel.CloseChannel();}for (auto &channel : channels){channel.Wait();}
}

a. 关闭所有的写端 b. 回收子进程

注意:一定是先关闭全部读端,然后统一读端,而不能关一个,等待一个。

在执行子进程创建逻辑的时候,是存在bug的。

我们在创建第一个信道的时候是没有什么问题的:

接着来看,我们创建第二个信道会发生什么:fd[4]的指向被第二个信道的子进程继承了,那么第一个管道的写端就个fd指向了,这就是坑

如果继续创建更多的信道,那么这种情况会一直累积,只有最后一个文件只有一个写端。如果我们是关一个等待一个,那么第一个信道,只会关闭一个读端,但还有其它的读端,这就会发生进程阻塞,导致管道文件一直在等待读取,无法被释放;如果统一的关闭读端,那么当关闭到最后一个信道的时候,最后一个信道读端只有一个,管道文件会被释放,然后就会递归式的逆向关闭其它信道的所有读端,最后将所有的管道文件释放,父进程只需要一 一等待,就能回收所有子进程了。


1.4进行测试

子进程执行任务成功,父进程回收子进程成功。


1.5完整代码

Task.hpp

#pragma once#include <iostream>
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>#define TaskNum 3typedef void (*task_t)(); // task_t 函数指针类型void Print()
{std::cout << "I am print task" << std::endl;
}
void DownLoad()
{std::cout << "I am a download task" << std::endl;
}
void Flush()
{std::cout << "I am a flush task" << std::endl;
}task_t tasks[TaskNum];void LoadTask()
{srand(time(nullptr) ^ getpid() ^ 17777);tasks[0] = Print;tasks[1] = DownLoad;tasks[2] = Flush;
}void ExcuteTask(int number)
{if (number < 0 || number > 2)return;tasks[number]();
}int SelectTask()
{return rand() % TaskNum;
}void work()
{while (true){int command = 0;int n = read(0, &command, sizeof(command));if (n == sizeof(int)){std::cout << "pid is : " << getpid() << " handler task" << std::endl;ExcuteTask(command);}else if (n == 0){std::cout << "sub process : " << getpid() << " quit" << std::endl;break;}}
}

test.cc

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"class Channel
{
public:Channel(int wfd, pid_t id, const std::string &name): _wfd(wfd), _subprocessid(id), _name(name){}int GetWfd() { return _wfd; }pid_t GetProcessId() { return _subprocessid; }std::string GetName() { return _name; }void CloseChannel(){close(_wfd);}void Wait(){pid_t rid = waitpid(_subprocessid, nullptr, 0);if (rid > 0){std::cout << "wait " << rid << " success" << std::endl;}}~Channel(){}private:int _wfd;pid_t _subprocessid;std::string _name;
};//  task_t task: 回调函数
void CreateChannelAndSub(int num, std::vector<Channel> *channels, task_t task)
{// BUG? --> fix bugfor (int i = 0; i < num; i++){// 1. 创建管道int pipefd[2] = {0};int n = pipe(pipefd);if (n < 0)exit(1);// 2. 创建子进程pid_t id = fork();if (id == 0){if (!channels->empty()){// 第二次之后,开始创建的管道for(auto &channel : *channels) channel.CloseChannel();}// child - readclose(pipefd[1]);dup2(pipefd[0], 0); // 将管道的读端,重定向到标准输入task();close(pipefd[0]);exit(0);}// 3.构建一个channel名称std::string channel_name = "Channel-" + std::to_string(i);// 父进程close(pipefd[0]);// a. 子进程的pid b. 父进程关心的管道的w端channels->push_back(Channel(pipefd[1], id, channel_name));}
}int NextChannel(int channelnum)
{static int next = 0;int channel = next;next++;next %= channelnum;return channel;
}void SendTaskCommand(Channel &channel, int taskcommand)
{write(channel.GetWfd(), &taskcommand, sizeof(taskcommand));
}
void ctrlProcessOnce(std::vector<Channel> &channels)
{sleep(1);// a. 选择一个任务int taskcommand = SelectTask();// b. 选择一个信道和进程int channel_index = NextChannel(channels.size());// c. 发送任务SendTaskCommand(channels[channel_index], taskcommand);std::cout << std::endl;std::cout << "taskcommand: " << taskcommand << " channel: "<< channels[channel_index].GetName() << " sub process: " << channels[channel_index].GetProcessId() << std::endl;
}
void ctrlProcess(std::vector<Channel> &channels, int times = -1)
{if (times > 0){while (times--){ctrlProcessOnce(channels);}}else{while (true){ctrlProcessOnce(channels);}}
}void CleanUpChannel(std::vector<Channel> &channels)
{for (auto &channel : channels){channel.CloseChannel();}for (auto &channel : channels){channel.Wait();}
}int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " processnum" << std::endl;return 1;}int num = std::stoi(argv[1]);LoadTask();std::vector<Channel> channels;// 1. 创建信道和子进程CreateChannelAndSub(num, &channels, work);// 2. 通过channel控制子进程ctrlProcess(channels, 10);// 3. 回收管道和子进程. a. 关闭所有的写端 b. 回收子进程CleanUpChannel(channels);// sleep(100);return 0;
}#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"class Channel
{
public:Channel(int wfd, pid_t id, const std::string &name): _wfd(wfd), _subprocessid(id), _name(name){}int GetWfd() { return _wfd; }pid_t GetProcessId() { return _subprocessid; }std::string GetName() { return _name; }void CloseChannel(){close(_wfd);}void Wait(){pid_t rid = waitpid(_subprocessid, nullptr, 0);if (rid > 0){std::cout << "wait " << rid << " success" << std::endl;}}~Channel(){}private:int _wfd;pid_t _subprocessid;std::string _name;
};//  task_t task: 回调函数
void CreateChannelAndSub(int num, std::vector<Channel> *channels, task_t task)
{// BUG? --> fix bugfor (int i = 0; i < num; i++){// 1. 创建管道int pipefd[2] = {0};int n = pipe(pipefd);if (n < 0)exit(1);// 2. 创建子进程pid_t id = fork();if (id == 0){if (!channels->empty()){// 第二次之后,开始创建的管道for(auto &channel : *channels) channel.CloseChannel();}// child - readclose(pipefd[1]);dup2(pipefd[0], 0); // 将管道的读端,重定向到标准输入task();close(pipefd[0]);exit(0);}// 3.构建一个channel名称std::string channel_name = "Channel-" + std::to_string(i);// 父进程close(pipefd[0]);// a. 子进程的pid b. 父进程关心的管道的w端channels->push_back(Channel(pipefd[1], id, channel_name));}
}int NextChannel(int channelnum)
{static int next = 0;int channel = next;next++;next %= channelnum;return channel;
}void SendTaskCommand(Channel &channel, int taskcommand)
{write(channel.GetWfd(), &taskcommand, sizeof(taskcommand));
}
void ctrlProcessOnce(std::vector<Channel> &channels)
{sleep(1);// a. 选择一个任务int taskcommand = SelectTask();// b. 选择一个信道和进程int channel_index = NextChannel(channels.size());// c. 发送任务SendTaskCommand(channels[channel_index], taskcommand);std::cout << std::endl;std::cout << "taskcommand: " << taskcommand << " channel: "<< channels[channel_index].GetName() << " sub process: " << channels[channel_index].GetProcessId() << std::endl;
}
void ctrlProcess(std::vector<Channel> &channels, int times = -1)
{if (times > 0){while (times--){ctrlProcessOnce(channels);}}else{while (true){ctrlProcessOnce(channels);}}
}void CleanUpChannel(std::vector<Channel> &channels)
{for (auto &channel : channels){channel.CloseChannel();}for (auto &channel : channels){channel.Wait();}
}int main(int argc, char *argv[])
{if (argc != 2){std::cerr << "Usage: " << argv[0] << " processnum" << std::endl;return 1;}int num = std::stoi(argv[1]);LoadTask();std::vector<Channel> channels;// 1. 创建信道和子进程CreateChannelAndSub(num, &channels, work);// 2. 通过channel控制子进程ctrlProcess(channels, 10);// 3. 回收管道和子进程. a. 关闭所有的写端 b. 回收子进程CleanUpChannel(channels);// sleep(100);return 0;
}

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

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

相关文章

Mysql(一)查询Sql是如何执行的

Hello&#xff0c;大家好我是极客涛&#x1f60e;&#xff0c;我最近在整理Mysql相关的知识点&#xff0c;所以准备开启一个Mysql的主线任务&#xff0c;大概耗时3周左右&#xff0c;整个节奏还是由浅入深&#xff0c;主要包括Mysql的架构、事务实现、索引组织形式、SQL优化、日…

图解大模型分布式并行各种通信原语

背景 在分布式集群上执行大模型任务时候&#xff0c;往往使用到数据并行&#xff0c;流水线并行&#xff0c;张量并行等技术&#xff0c;这些技术本质上也就是对数据进行各种方案的切分&#xff0c;然后放到不同的节点上运算。不同节点在计算的过程中需要对数据分发或者同步等…

python的一种集成开发工具:PyCharm开发工具

一. 简介 本文简单了解两种 python语言所使用的 集成开发环境&#xff1a; PyCharm、vscode。 python语言学习中&#xff0c;可以任意选中这两个集成开发环境的一种就可以。本文先来简单学习 PyCharm开发工具安装与使用。 二. python的一种集成开发工具&#xff1a;PyChar…

实现Redis和数据库数据同步问题(JAVA代码实现)

这里我用到了Redis当中的发布订阅模式实现(JAVA代码实现) 先看图示 下面为代码实现 首先将RedisMessageListenerContainer交给Spring管理. Configuration public class redisConfig {AutowiredRedisConnectionFactory redisConnectionFactory;AutowiredQualifier("car…

Linux线程:线程分离

目录 一、什么是线程分离 1.1pthread_detach 1.2pthread线程库存在的意义 1.3__thread线程的局部存储 1.4系统调用clone 一、什么是线程分离 1.1pthread_detach 默认情况下&#xff0c;新创建的线程是joinable的&#xff0c;线程退出后&#xff0c;需要对其进行pthread_joi…

数据标准的制定落地

目录 什么是数据标准 基本定义 目的 数据标准体系分类 从内容层面分类 从管理视角分类 从面向的对象分类 从数据结构的角度分类 数据标准价值 业务价值 技术价值 管理价值 数据标准和数据治理的关系 数据标准在数据治理各项任务中的作用 数据标准与主数据 数据…

车联网安全入门——ICSim模拟器使用

文章目录 车联网安全入门——ISCim模拟器使用介绍主要特点&#xff1a;使用场景&#xff1a; 安装使用捕获can流量candumpcansnifferwiresharkSavvyCAN主要特点&#xff1a;使用场景&#xff1a; 重放can报文cansendSavvyCAN 总结 车联网安全入门——ISCim模拟器使用 &#x1…

LabVIEW步进电机的串口控制方法与实现

本文介绍了在LabVIEW环境中通过串口控制步进电机的方法&#xff0c;涵盖了基本的串口通信原理、硬件连接步骤、LabVIEW编程实现以及注意事项。通过这些方法&#xff0c;用户可以实现对步进电机的精确控制&#xff0c;适用于各种自动化和运动控制应用场景。 步进电机与串口通信…

【刷题(15】普通数组

一 普通数组基础 首先&#xff0c;我们根据下图先了解一下什么是前缀和。 既然我们明白了前缀和是怎么回事&#xff0c;那我们就来看一下我们该怎么输入 先给出答案&#xff0c;然后再给出分析。 答案&#xff1a; for (int i 1; i < n; i ){cin >> a[i];s[i] s…

Pytest框架中用例用例执行常用参数介绍

pytest 支持通过命令行参数来定制测试运行的方式。以下是一些常用的 pytest 执行参数介绍。 学习目录 -q 或 --quiet: 安静模式&#xff0c;只显示进度和摘要 -s : 选项允许在测试的输出中捕获 stdout 和 stderr。 -v : 选项会使 pytest 的输出更加详细。 -k &#xff1a;…

DIYP对接骆驼后台IPTV管理,退出菜单中显示用户名已经网络信息,MAC,剩余天数,套餐名称等

演示&#xff1a;https://url03.ctfile.com/f/1779803-1042599473-4dc000?p8976 (访问密码: 8976) 后台加上EPG&#xff0c;增加一些播放源的动态端口替换。 前台app上&#xff0c;退出菜单中显示用户名已经网络信息&#xff0c;MAC&#xff0c;剩余天数&#xff0c;套餐名称…

QT之常用控件

一个图形化界面当然需要有各种各样的控件&#xff0c;QT也不例外&#xff0c;在QT designer中就有提供各种各样的控件&#xff0c;用以开发图形化界面。 而想使用好一个QT控件&#xff0c;就需要了解这些控件。 QWidget 在QT中&#xff0c;所有控件都继承自 QWidget 类&…

中间件模版引擎

文章目录 中间件1.自定义中间件1&#xff09;全局2&#xff09;局部中间件 2.内置中间件(静态资源目录&#xff09; Art-template1.模板语法1&#xff09;输出2&#xff09;原文输出3&#xff09;条件判断4&#xff09;循环5&#xff09;子模版6&#xff09;模版继承7&#xff…

git远程仓库限额的解决方法——大文件瘦身

Git作为世界上最优秀的分布式版本控制工具&#xff0c;也是优秀的文件管理工具&#xff0c;它赋予了项目成员对项目进行远程协同开发能力&#xff0c;因此受到越来越多的行业从业人员的喜爱。很多优秀的项目管理平台&#xff0c;比如国内的Gitee&#xff0c;国外的Github&#…

Django表单革命:打造安全、高效、用户友好的Web应用

Django表单处理&#xff0c;听起来是不是有点枯燥&#xff1f;别急&#xff0c;阿佑将带你领略Django表单的艺术之美。我们将以轻松幽默的语言&#xff0c;一步步引导你从表单的创建到管理&#xff0c;再到验证和自定义&#xff0c;让你在不知不觉中掌握Django表单的精髓。文章…

SpringMVC:转发和重定向

1. 请求转发和重定向简介 参考该链接第9点 2. forward 返回下一个资源路径&#xff0c;请求转发固定格式&#xff1a;return "forward:资源路径"如 return "forward:/b" 此时为一次请求返回逻辑视图名称 返回逻辑视图不指定方式时都会默认使用请求转发in…

留给“端侧大模型”的时间不多了

端侧大模型&#xff08;Edge AI models&#xff09;&#xff0c;也就是只在设备本地&#xff08;如智能手机、IoT设备、嵌入式系统等&#xff09;运行的大模型&#xff0c;过去一两年来非常流行。 具体表现在&#xff0c;终端设备厂商&#xff0c;如苹果、荣耀、小米、OV等&…

【操作与配置】VS2017与MFC环境配置

【操作与配置】VS2017与MFC环境配置 概述 Visual Studio 是一款强大且多功能的集成开发环境&#xff08;IDE&#xff09;&#xff0c;适用于软件开发人员和团队。使用此应用程序&#xff0c;您可以构建和调试现代Web应用程序&#xff0c;并利用扩展帮助探索几乎任何编程语言。…

重学java 55. 集合 Set接口

我救自己万万次&#xff0c;铮铮劲草&#xff0c;绝不动摇 —— 24.6.2 一、Set集合介绍 Set和Map密切相关的 Map的遍历需要先变成单列集合&#xff0c;只能变成set集合 二、HashSet集合的介绍和使用 1.概述 HashSet是Set接口的实现类 2.特点 a、元素唯一 b、元素无序 c、无索引…

devicemotion 或者 deviceorientation在window.addEventListener 事件中不生效,没有输出内容

问题&#xff1a;devicemotion 或者 deviceorientation 在window.addEventListener 事件中不生效&#xff0c;没有输出内容 原因&#xff1a; 1、必须在Https协议下才可使用 2、必须用户手动点击click事件中调用 &#xff0c;进行权限申请 源码&#xff1a; <!DOCTYPE h…