cpp随笔——如何实现一个简单的进程心跳功能

什么是进程的心跳

在我们日常后台服务程序运行中,一般是调度模块,进程心跳以及进程监控共同工作,进而实现实现服务的稳定运行,在前面我们介绍过如何去实现一个简单的调度模块,而今天我们所要介绍的就是如何实现进程的心跳,首先什么是进程的心跳呢?进程的心跳机制是一种监控进程健康状况的方法,通常用于服务端应用或者需要长期运行的后台守护进程(daemon,我们在后台服务程序在运行中由于有很多功能需要运行,所以一般是以多进程的方式来同时运行多个后台服务程序,那么问题来了,当有多个后台服务程序在运行时我们如何来确定它们的运行状态呢?这就是进程心跳的功能了,进程心跳,顾名思义,就是我们用来判断进程是否正常运行的一种手段,我们首先定义一个心跳信息结构体,如下:

struct stprocinfo  //存储进程心跳信息的结构体
{
    int pid;  //进程编号
    char name[50]={0}; //进程名称
    int timeout;  //超时时间
    time_t atime; //最后一次心跳时间
    stprocinfo() =default;  //默认构造函数 
    stprocinfo(int pid,char* name,int timeout,time_t atime):pid(pid),timeout(timeout),atime(atime)
    {
        strcpy(this->name,name);
    }
};

结构体中存储了进程的相关信息,同时我们会通过atimetimeout来判断结构体是否是正常运行,下面我们来看一下具体的实现过程。

进程心跳的初步实现

在这里插入图片描述
上面是一个进程心跳的初步实现,主要是以下几步:

  1. 创建共享内存
 //创建共享内存
   shmid=shmget((key_t)0X5550,sizeof(struct stprocinfo)*1000,IPC_CREAT|0666);
   if (shmid==-1)
   {
       perror("创建共享内存失败");
       return -1;
   }
   cout<<"shmid="<<shmid<<endl;
  1. 将共享内存连接到当前进程的地址空间
    //将共享内存连接到当前进程的地址空间
   m_shm=(stprocinfo*)shmat(shmid,nullptr,0);
   if(m_shm==(void*)-1)
   {
       perror("共享内存连接失败");
       return -1;
   }
   cout<<"shmat ok"<<endl;
  1. 遍历共享内存,寻找空闲位置

   //遍历当前共享内存的占用情况,主要用来调试
   for(int i=0;i<1000;i++)
   {
       if(m_shm[i].pid!=0)
       {
           cout<<"pid="<<m_shm[i].pid<<" "
           <<"name="<<m_shm[i].name<<" "
           <<"timeout="<<m_shm[i].timeout<<" "
           <<"atime="<<m_shm[i].atime<<endl;
       }
   }

   stprocinfo info(getpid(),"server1",30,time(0));

   //在共享内存中查找是否有空闲的地址
   for(int i=0;i<1000;i++)
   {
       if(m_shm[i].pid==0)
       {
           m_pos=i;
           cout<<"找到空闲地址,m_pos="<<m_pos<<endl;
           break;
       }
   }

   if(m_pos==-1)
   {
       cout<<"共享内存已满"<<endl;
       Exit(-1);
   }

   memcp
  1. 程序运行并且更新进程心跳
    while(1)
   {
       //写入程序的运行逻辑
       sleep(10);  //模拟程序运行
       m_shm[m_pos].atime=time(0);  //更新当前进程的心跳时间
   }

完整代码如下:

#include "../../public/_public.h"

using namespace idc;   //自己封装的命名空间

struct stprocinfo  //存储进程心跳信息的结构体
{
    int pid;  //进程编号
    char name[50]={0}; //进程名称
    int timeout;  //超时时间
    time_t atime; //最后一次心跳时间
    stprocinfo() =default;  //默认构造函数 
    stprocinfo(int pid,char* name,int timeout,time_t atime):pid(pid),timeout(timeout),atime(atime)
    {
        strcpy(this->name,name);
    }
};

int shmid=-1; //共享内存的ID
stprocinfo* m_shm=nullptr;//指向共享内存的地址
int m_pos=-1; //记录当前进程在共享内存的位置
void Exit(int sig); //信号处理函数

int main()
{
    //信号处理
    signal(SIGINT,Exit);
    signal(SIGTERM,Exit);

    //创建共享内存
    shmid=shmget((key_t)0X5550,sizeof(struct stprocinfo)*1000,IPC_CREAT|0666);
    if (shmid==-1)
    {
        perror("创建共享内存失败");
        return -1;
    }
    cout<<"shmid="<<shmid<<endl;

    //将共享内存连接到当前进程的地址空间
    m_shm=(stprocinfo*)shmat(shmid,nullptr,0);
    if(m_shm==(void*)-1)
    {
        perror("共享内存连接失败");
        return -1;
    }
    cout<<"shmat ok"<<endl;

    //遍历当前共享内存的占用情况,主要用来调试
    for(int i=0;i<1000;i++)
    {
        if(m_shm[i].pid!=0)
        {
            cout<<"pid="<<m_shm[i].pid<<" "
            <<"name="<<m_shm[i].name<<" "
            <<"timeout="<<m_shm[i].timeout<<" "
            <<"atime="<<m_shm[i].atime<<endl;
        }
    }

    stprocinfo info(getpid(),"server1",30,time(0));

    //在共享内存中查找是否有空闲的地址
    for(int i=0;i<1000;i++)
    {
        if(m_shm[i].pid==0)
        {
            m_pos=i;
            cout<<"找到空闲地址,m_pos="<<m_pos<<endl;
            break;
        }
    }

    if(m_pos==-1)
    {
        cout<<"共享内存已满"<<endl;
        Exit(-1);
    }

    memcpy(&m_shm[m_pos],&info,sizeof(struct stprocinfo));  //将当前进程的心跳信息存入共享内存

    while(1)
    {
        //写入程序的运行逻辑
        sleep(10);  //模拟程序运行
        m_shm[m_pos].atime=time(0);  //更新当前进程的心跳时间
    }
    return 0;
}

void Exit(int sig)
{
    cout<<"收到信号:"<<sig<<endl;
    //清除共享内存中的心跳信息结构体
    if(m_pos!=-1)
    {
        memset(m_shm+m_pos,0,sizeof(struct stprocinfo));
        m_pos=-1;
    }
    //将共享内存分离出来
    if(m_shm!=nullptr)
    {
        shmdt(m_shm);
    }
    exit(0);
}

编译代码如下:

# 开发框架头文件路径
PUBINCL = -I/root/mylib/project/public

# 开发框架cpp文件名,这里直接包含进来,没有采用链接库,是为了方便调试。
PUBLICPP = /root/mylib/project/public/_public.cpp
# 编译参数
CFLAGS= -g

all: demo1

demo1:demo1.cpp
	g++ $(CFLAGS) -o demo1 demo1.cpp $(PUBLICPP)

clean:
	rm -f demo1

注明
以上仅供参考,大家根据自己的需求来进行修改。

测试并且运行,这里我们开启三个命令行来测试:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们看到基本实现了我们想的功能,将带有进程心跳的信息储存到了结构体中,后面我们会介绍我们如何基于守护进程来对进程的心跳来实现对进程状态的监控,不过这是后话了。

进程心跳代码的优化

上面我们已经实现了一个简单的进程心跳代码,但是现在有几个实际情况需要我们来考虑,分别是:

  • 这里我们关于退出信号的处理是无法处理kill -9这种异常退出的,这个时候我们的Exit函数是无法正常发挥功能的,同时也会导致该进程的心跳信息残留在共享内存中,所以我们要对代码进行优化:
    //在共享内存中查找是否有空闲的地址

    for(int i=0;i<1000;i++)  //如果有与该进程pid相同,说明有进程未清理干净
    {
        if(m_shm[i].pid==info.pid)
        {
            cout<<"找到旧进程"<<endl;
            m_pos=i;
            break;
        }
    }
    
    if(m_pos==-1)
    {
        for(int i=0;i<1000;i++)
        {
            if(m_shm[i].pid==0)
            {
                m_pos=i;
                cout<<"找到空闲地址,m_pos="<<m_pos<<endl;
                break;
            }
        }
    }

    if(m_pos==-1)
    {
        cout<<"共享内存已满"<<endl;
        Exit(-1);
    }

  • 这里我们要求的使用场景是多个进程在同时运行,那么问题来了,如果同时多个程序运行同时向共享内存的一块地址写怎么办?这里我们可以基于进程同步来解决这个问题,而向实现进程同步,就需要信号量了,这里我使用的是已经封装好的信号量,代码如下:
    class csemp
    {
    private:
        union semun // 用于操控共享内存的联合体
        {
            int value;
            struct semid_ds *buf;
            unsigned short *arry;
        };
        int m_semid; // 信号量id

        /*如果将m_semflg设为SEM_UNOD,操作系统将跟踪进程对信号量的修改,在全部修改过信号量的进程终止后将信号量设置为初始值
        m_semflag=1时用于互斥锁,m_semflag=0时用于生产消费者模型*/
        short m_semflg;                           // 信号量值
        csemp(const csemp &) = delete;            // 禁用拷贝构造函数
        csemp &operator=(const csemp &) = delete; // 禁用赋值运算符

    public:
        csemp() : m_semid(-1) {}
        /*如果信号量已存在,就获取信号量
        如果信号量不存在,就创建信号量并将其初始化为value
        互斥锁时,value=1,semflag=SEM_UNOD
        生产消费者模型时,value=0,semflag=0*/
        bool init(key_t key, unsigned short value = 1, short semflg = SEM_UNDO);
        bool wait(short value = -1); // P操作
        bool post(short value = 1);  // V操作
        int getvalue();
        bool destroy();
        ~csemp();
    };


        bool csemp::init(key_t key, unsigned short value, short semflg)
    {
        if (m_semid != -1) // 信号量已经初始化了
        {
            return false;
        }
        m_semflg = semflg;
        if ((m_semid = semget(key, 1, 0666)) == -1) // 尝试获取信号量
        {
            if (errno == ENOENT) // 未找到信号量
            {
                if ((m_semid = semget(key, 1, IPC_CREAT | 0666 | IPC_EXCL)) == -1) // 创建信号量
                {
                    if (errno == EEXIST) // 信号量已存在
                    {
                        if ((m_semid = semget(key, 1, 0666)) == -1)
                        {
                            perror("init semget 1");
                            return false;
                        }
                        return true;
                    }
                    else
                    {
                        perror("init semget 2");
                        return false;
                    }
                }
                union semun b;
                b.value = value;
                if (semctl(m_semid, 0, SETVAL, b) == -1) // 设置信号量初值
                {
                    perror("init semctl");
                    return false;
                }
            }
            else
            {
                perror("init semget 3");
                return false;
            }
        }
        return true;
    }

    bool csemp::wait(short value)
    {
        if (m_semid == -1)
            return false;
        struct sembuf s;
        s.sem_num = 0;
        s.sem_op = value;
        s.sem_flg = m_semflg;
        if (semop(m_semid, &s, 1) == -1)
        {
            perror("wait semop");
            return false;
        }
        return true;
    }

    bool csemp::post(short value)
    {
        if (m_semid == -1)
            return false;
        struct sembuf s;
        s.sem_num = 0;
        s.sem_op = value;
        s.sem_flg = m_semflg;
        if (semop(m_semid, &s, 1) == -1)
        {
            perror("post semop");
            return false;
        }
        return true;
    }

    int csemp::getvalue()
    {
        return semctl(m_semid, 0, GETVAL);
    }

    bool csemp::destroy()
    {
        if (m_semid == -1)
            return false;
        if (semctl(m_semid, 0, IPC_RMID) == -1)
        {
            perror("destroy semctl");
            return false;
        }
        return true;
    }

    csemp::~csemp()
    {
    }

实现代码如下:

 csemp shmlock;
    if(shmlock.init(0x5550)==-1)
    {
        cout<<"shmlock init error"<<endl;
        Exit(-1);
    }

    shmlock.wait(); //加锁

    //在共享内存中查找是否有空闲的地址

    for(int i=0;i<1000;i++)
    {
        if(m_shm[i].pid==info.pid)
        {
            cout<<"找到旧进程"<<endl;
            m_pos=i;
            break;
        }
    }

    if(m_pos==-1)
    {
        for(int i=0;i<1000;i++)
        {
            if(m_shm[i].pid==0)
            {
                m_pos=i;
                cout<<"找到空闲地址,m_pos="<<m_pos<<endl;
                break;
            }
        }
    }

    if(m_pos==-1)
    {
        cout<<"共享内存已满"<<endl;
        shmlock.post(); //解锁
        Exit(-1);
    }

    memcpy(&m_shm[m_pos],&info,sizeof(struct stprocinfo));  //将当前进程的心跳信息存入共享内存
    shmlock.post(); //解锁

最后我们就实现一个基本的可以实现多进程监控的进程心跳程序了,在最后我们可以将它封装成一个简单的类,供我们以后使用:

    // 进程心跳有关的类

    struct st_procinfo // 存储进程心跳信息的结构体
    {
        int pid;                // 进程编号
        char name[50] = {0};    // 进程名称
        int timeout;            // 超时时间
        time_t atime;           // 最后一次心跳时间
        st_procinfo() = default; // 默认构造函数
        st_procinfo(int pid, char *name, int timeout, time_t atime) : pid(pid), timeout(timeout), atime(atime)
        {
            strcpy(this->name, name);
        }
    };

    #define MAXNUM 1000;   // 最大进程数
    #define SHMKEYP 0x5095 // 共享内存的key。
    #define SEMKEYP 0x5095 // 信号量的key。

    class cpactive // 实现进程心跳的类
    {
    private:
        int shmid;
        int m_pos;
        st_procinfo *m_shm;

    public:
        cpactive();
        bool init(int timeout,string pname="",clogfile *logfile=nullptr);  //这里传指针视为了选择是否使用日志打印
        bool update();  //更新心跳时间
        ~cpactive();
    };


	 cpactive::cpactive()
 {
     m_shmid=0;
     m_pos=-1;
     m_shm=0;
 }

 // 把当前进程的信息加入共享内存进程组中。
 bool cpactive::addpinfo(const int timeout,const string &pname,clogfile *logfile)
 {
    if (m_pos!=-1) return true;

    // 创建/获取共享内存,键值为SHMKEYP,大小为MAXNUMP个st_procinfo结构体的大小。
    if ( (m_shmid = shmget((key_t)SHMKEYP, MAXNUMP*sizeof(struct st_procinfo), 0666|IPC_CREAT)) == -1)
    { 
        if (logfile!=nullptr) logfile->write("创建/获取共享内存(%x)失败。\n",SHMKEYP); 
        else printf("创建/获取共享内存(%x)失败。\n",SHMKEYP);

        return false; 
    }

    // 将共享内存连接到当前进程的地址空间。
    m_shm=(struct st_procinfo *)shmat(m_shmid, 0, 0);
  
    /*
    struct st_procinfo stprocinfo;    // 当前进程心跳信息的结构体。
    memset(&stprocinfo,0,sizeof(stprocinfo));
    stprocinfo.pid=getpid();            // 当前进程号。
    stprocinfo.timeout=timeout;         // 超时时间。
    stprocinfo.atime=time(0);           // 当前时间。
    strncpy(stprocinfo.pname,pname.c_str(),50); // 进程名。
    */
    st_procinfo stprocinfo(getpid(),pname.c_str(),timeout,time(0));    // 当前进程心跳信息的结构体。

    // 进程id是循环使用的,如果曾经有一个进程异常退出,没有清理自己的心跳信息,
    // 它的进程信息将残留在共享内存中,不巧的是,如果当前进程重用了它的id,
    // 守护进程检查到残留进程的信息时,会向进程id发送退出信号,将误杀当前进程。
    // 所以,如果共享内存中已存在当前进程编号,一定是其它进程残留的信息,当前进程应该重用这个位置。
    for (int ii=0;ii<MAXNUMP;ii++)
    {
        if ( (m_shm+ii)->pid==stprocinfo.pid ) { m_pos=ii; break; }
    }

    csemp semp;                       // 用于给共享内存加锁的信号量id。

    if (semp.init(SEMKEYP) == false)  // 初始化信号量。
    {
        if (logfile!=nullptr) logfile->write("创建/获取信号量(%x)失败。\n",SEMKEYP); 
        else printf("创建/获取信号量(%x)失败。\n",SEMKEYP);

        return false;
    }

    semp.wait();  // 给共享内存上锁。

    // 如果m_pos==-1,表示共享内存的进程组中不存在当前进程编号,那就找一个空位置。
    if (m_pos==-1)
    {
        for (int ii=0;ii<MAXNUMP;ii++)
            if ( (m_shm+ii)->pid==0 ) { m_pos=ii; break; }
    }

    // 如果m_pos==-1,表示没找到空位置,说明共享内存的空间已用完。
    if (m_pos==-1) 
    { 
        if (logfile!=0) logfile->write("共享内存空间已用完。\n");
        else printf("共享内存空间已用完。\n");

        semp.post();  // 解锁。

        return false; 
    }

    // 把当前进程的心跳信息存入共享内存的进程组中。
    memcpy(m_shm+m_pos,&stprocinfo,sizeof(struct st_procinfo)); 

    semp.post();   // 解锁。

    return true;
 }

 // 更新共享内存进程组中当前进程的心跳时间。
 bool cpactive::uptatime()
 {
    if (m_pos==-1) return false;

    (m_shm+m_pos)->atime=time(0);

    return true;
 }

 cpactive::~cpactive()
 {
    // 把当前进程从共享内存的进程组中移去。
    if (m_pos!=-1) memset(m_shm+m_pos,0,sizeof(struct st_procinfo));

    // 把共享内存从当前进程中分离。
    if (m_shm!=0) shmdt(m_shm);
 }

至此一个简单的进程心跳类就封装号了,后面我会介绍如何基于守护进程,进程心跳和调度模块来实现对进程的监控,大家下篇见!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MCU中如何利用串口通信,增加AT指令框架

第一步&#xff0c;通过串口与PC端建立通信第二步&#xff0c;根据PC端发来的AT指令&#xff0c;MCU执行相应代码 主要是解析PC端发来的字符串&#xff0c;也就是获取字符串、处理字符串、以及分析字符串。 1. 串口通信 用到的是DMA串口通信&#xff0c;收发字符串数据时&…

什么是JavaScript中的箭头函数(arrow functions)?

聚沙成塔每天进步一点点 本文回顾 ⭐ 专栏简介什么是JavaScript中的箭头函数&#xff08;arrow functions&#xff09;&#xff1f;1. 引言2. 箭头函数的语法2.1 基本语法2.2 示例 3. 箭头函数的特点3.1 简洁的语法3.2 没有this绑定3.3 不能用作构造函数3.4 没有arguments对象3…

基于SpringBoot的就业信息管理系统

你好&#xff0c;我是计算机学姐码农小野&#xff01;如果你对就业信息管理系统感兴趣或有相关需求&#xff0c;欢迎私信联系我。 开发语言&#xff1a; Java 数据库&#xff1a; MySQL 技术&#xff1a; SpringBootMySql 工具&#xff1a; MyEclipse、Tomcat 系统展示…

隐私信息管理体系认证:守护个人信息,筑牢隐私防线

在数字化浪潮汹涌的当下&#xff0c;个人信息安全问题愈发凸显其重要性。随着互联网技术的飞速发展&#xff0c;我们的隐私信息如同裸露在阳光下的沙滩&#xff0c;稍有不慎就可能被不法分子窃取或滥用。因此&#xff0c;构建一个完善的隐私信息管理体系&#xff0c;成为了保障…

结合数据索引结构看SQL的真实执行过程

引言 关于数据库设计与优化的前几篇文章中&#xff0c;我们提到了数据库设计优化应该遵守的指导原则、数据库底层的索引组织结构、数据库的核心功能组件以及SQL的解析、编译等。这些其实都是在为SQL的优化、执行的理解打基础。 今天这篇文章&#xff0c;我们以MySQL中InnoDB存…

软件测评机构:关于软件验收测试作用与实施步骤全解析

软件验收测试是指在软件项目交付给用户之前进行的一系列测试活动&#xff0c;其主要目的是验证软件是否符合用户需求和设计规范&#xff0c;以确保软件的质量和稳定性。 软件验收测试在软件开发生命周期的最后阶段进行&#xff0c;起到了至关重要的作用。它能够帮助客户确认软…

AI PC(智能电脑)技术分析

一文看懂AI PC&#xff08;智能电脑&#xff09; 2024年&#xff0c;英特尔、英伟达等芯片巨头革新CPU技术&#xff0c;融入AI算力&#xff0c;为传统PC带来质的飞跃&#xff0c;引领智能计算新时代。 2024年&#xff0c;因此被叫作人工智能电脑&#xff08;AI PC&#xff09;…

【elementui】记录解决el-tree开启show-checkbox后,勾选一个叶结点后会自动折叠的现象

第一种解决方案&#xff1a;设置default-expand-keys的值为当前选中的key值即可 <el-treeref"tree"class"checkboxSelect-wrap":data"treeData"show-checkboxnode-key"id":expand-on-click-node"true":props"defau…

MATLAB——循环语句

一、for end语句 在该语法中&#xff0c;循环变量是用于迭代的变量名&#xff0c;它会在每次循环迭代中从向量或矩阵中取出一列的值。数值向量或者矩阵则表示了循环变量可以取值的范围&#xff0c;通常根据实际需要事先给定。一旦循环变量遍历完数值向量或者矩阵中的所有值&…

初试成绩占比百分之70!计算机专硕均分340+!华中师范大学计算机考研考情分析!

华中师范大学&#xff08;Central China Normal University&#xff09;简称“华中师大”或“华大”&#xff0c;位于湖北省会武汉&#xff0c;是中华人民共和国教育部直属重点综合性师范大学&#xff0c;国家“211工程”、“985工程优势学科创新平台”重点建设院校&#xff0c…

苹果公司的Wifi定位服务(WPS)存在被滥用的风险

安全博客 Krebs on Security 2024年5月21日发布博文&#xff0c;表示苹果公司的定位服务存在被滥用风险&#xff0c;通过 "窃取"WPS 数据库&#xff0c;可以定位部队行踪。 相关背景知识 手机定位固然主要依赖卫星定位&#xff0c;不过在城市地区&#xff0c;密集的…

YOLOv10全网最新创新点改进系列:融合GSConv+Slim Neck,双改进、双增强,替换特征融合层实现, 轻量化涨点改进策略,有效涨点神器!

YOLOv10全网最新创新点改进系列&#xff1a;融合GSConvSlim Neck&#xff0c;双改进、双增强&#xff0c;替换特征融合层实现&#xff0c; 轻量化涨点改进策略&#xff0c;有效涨点神器&#xff01; 所有改进代码均经过实验测试跑通&#xff01;截止发稿时YOLOv10已改进40&…

vue中的坑·

常规 1.使用watch时&#xff0c;immediate true会在dom挂载前执行 2.使用this.$attrs和props 可以获取上层非原生属性&#xff08;class/id&#xff09; 多层次嵌套引用 设置的时候直接赋值&#xff0c;修改的时候即使用的双向绑定加上$set / nextick / fouceUpdate都不会同步…

MySQL表的练习

二、创建表 1、创建一个名称为db_system的数据库 create database db_system; 2、在该数据库下创建两张表&#xff0c;具体要求如下 员工表 user 字段 类型 约束 备注 id 整形 主键&#xff0c;自增长 id N…

探索设计的未来:了解设计师对生成式人工智能(AIGC)工具的采用

在数字化浪潮的推动下&#xff0c;设计行业正经历着一场革命性的变革。随着生成式人工智能&#xff08;AIGC&#xff09;技术的发展&#xff0c;设计师们迎来了前所未有的机遇与挑战。这些工具不仅重塑了传统的设计流程&#xff0c;还为设计师们提供了更广阔的创意空间和更高效…

vue模板语法v-html

模板语法v-html vue使用一种基于HTML的模板语法&#xff0c;使我们能够声明式的将其组件实例的数据绑定到呈现的DOM上&#xff0c;所有的vue模板都是语法层面的HTML&#xff0c;可以被符合规范的浏览器和HTML解释器解析。 一.文本插值 最基本的数据绑定形式是文本插值&#…

理解神经网络的通道数

理解神经网络的通道数 1. 神经网络的通道数2. 输出的宽度和长度3. 理解神经网络的通道数3.1 都是错误的图片惹的祸3.1.1 没错但是看不懂的图3.1.2 开玩笑的错图3.1.3 给人误解的图 3.2 我或许理解对的通道数3.2.1 动图演示 1. 神经网络的通道数 半路出嫁到算法岗&#xff0c;额…

【算法训练记录——Day41】

Day41——动态规划Ⅲ 1.理论基础——代码随想录2.纯01背包_[kamacoder46](https://kamacoder.com/problempage.php?pid1046)3.leetcode_416分割等和子集 背包&#xff01;&#xff01; 1.理论基础——代码随想录 主要掌握01背包和完全背包 物品数量&#xff1a; 只有一个 ——…

顶级5款有用的免费IntelliJ插件,提升你作为Java开发者的旅程

在本文中&#xff0c;我们将深入探讨IntelliJ IDEA插件——那些可以提升你生产力的神奇附加组件&#xff0c;并微调你的代码以达到卓越。我们将探索5款免费插件&#xff0c;旨在将你的开发水平提升到一个新的高度。 1. Test Data 使用Test Data插件进行上下文操作 作为开发者&a…
最新文章