linux入门---用匿名管道实现一个功能

news2024/12/23 16:06:56

前言

在之前的学习中我们知道通信的概念和匿名管道的使用,那么接下来我们就要用匿名管道来实现一个功能,首先我们有很多的函数需要被执行,然后创建一些子进程通过匿名管道方式给子进程传递一些信息,然后子进程就根据这些信息来确定要执行的函数并执行,比如说下面的图片:
在这里插入图片描述
那么接下来我们就要一步一步的实现这个功能。

第一步:创建子进程并连接管道

首先我们要定义一个宏用来表示要创建多少个子进程来执行不同的任务,然后在main函数里面首先创建一个for循环用来不停的创建子进程,在循环的开始先创建一个数组用来记录管道的写端和读端,然后使用pipe函数创建一个匿名管道,因为管道可能会创建失败,所以创建一个变量用来记录pipe函数的返回值,如果变量的值等于0的话就断言一下,然后就可以使用fork函数来创建子进程,并根据fork函数的返回值来将父子进程执行的代码进行分开,那么这里的代码如下:

#include<iostream>
#include<unistd.h>
#include<cassert>
#define PROCSS_NUM 3
using namespace std;
int main()
{
    for(int i=0;i<PROCSS_NUM;i++)
    {
        int fds[2]={0};
        int n=pipe(fds);
        assert(n==0);
        (void)n;
        pid_t id=fork();
        if(id==0)
        {
            //这里是子进程执行的代码

        }
        //这里是父进程执行的代码
    }
    return 0;
}

然后在子进程里面干的第一件事就是关闭管道文件的写端,并且创建一个while循环,在循环里面要不停的读取管道里面的内容并且根据读取的内容执行对应的函数,那么这里的代码如下:

 if(id==0)
 {
     //这里是子进程执行的代码
     close(fds[1]);
     while(true)
     {
         //读取管道里面的内容

         //根据管道的内容执行对应的函数
     }
 }

因为这里存在多个子进程每个子进程都要有对应的写端 pid 和名字,所以为了方便描述这些子进程我们可以创建一个结构体来对其进行描述:

class subEp // 描述子进程
{
public:
    subEp(pid_t subId, int writeFd)
        : subId_(subId), writeFd_(writeFd)
    {
        char nameBuffer[1024];
        snprintf(nameBuffer, sizeof nameBuffer, "process-%d[pid(%d)-fd(%d)]", num++, subId_, writeFd_);
        //给子进程创建名字
        name_ = nameBuffer;
    }

public:
    static int num;//用于区别每个子进程
    std::string name_;//记录子进程的名字
    pid_t subId_;//子进程的id
    int writeFd_;//子进程的写端
};

那么创建完子进程之后父进程就得关闭管道的读端并且创建一个描述子进程的结构体对象,因为子进程存在多个所以描述子进程的对象也会存在多个,所以为了方便管理我们就得创建一个vector对象来管理结构体对象,那么目前的代码就如下:

#include<iostream>
#include<unistd.h>
#include<cassert>
#include<string>
#include<vector>
#define PROCSS_NUM 10
using namespace std;
class subEp // 描述子进程
{
public:
    subEp(pid_t subId, int writeFd)
        : subId_(subId), writeFd_(writeFd)
    {
        char nameBuffer[1024];
        snprintf(nameBuffer, sizeof nameBuffer, "process-%d[pid(%d)-fd(%d)]", num++, subId_, writeFd_);
        //给子进程创建名字
        name_ = nameBuffer;
    }

public:
    static int num;//用于区别每个子进程
    std::string name_;//记录子进程的名字
    pid_t subId_;//子进程的id
    int writeFd_;//子进程的写端
};

int subEp::num = 0;
int main()
{
    vector<subEP> subs;
    for(int i=0;i<PROCSS_NUM;i++)
    {
        int fds[2]={0};
        int n=pipe(fds);
        assert(n==0);
        (void)n;
        pid_t id=fork();
        if(id==0)
        {
            //这里是子进程执行的代码
            close(fds[1]);
            while(true)
            {
                //读取管道里面的内容

                //根据管道的内容执行对应的函数
            }

        }
        //这里是父进程执行的代码
        close[fds[0]];
        subs.push_back(subEP(id,fds[1]));

    }
    return 0;
}

我们上面写的是子进程和管道的创建过程,那么我们可以把这个模块写成一个函数,因为这个函数要对subs对象进行修改,并且子进程还有执行对应的函数,所以这个函数需要两个参数,一个是指向vector对象的指针,一个是指向函数集合的引用,那么这里的代码如下:

void createSubProcess(std::vector<subEp> *subs, std::vector<func_t> &funcMap)
{
     for(int i=0;i<PROCSS_NUM;i++)
    {
        int fds[2]={0};
        int n=pipe(fds);
        assert(n==0);
        (void)n;
        pid_t id=fork();
        if(id==0)
        {
            //这里是子进程执行的代码
            close(fds[1]);
            while(true)
            {
                //读取管道里面的内容

                //根据管道的内容执行对应的函数
            }

        }
        //这里是父进程执行的代码
        close[fds[0]];
        subs.push_back(subEP(id,fds[1]));
    }
}

第二步:建立被执行函数的集合

这里我们就可以创建很多函数,并且每个函数里面我们都可以添加一个打印一些话用来表示当前函数已经被执行了,那么这里的代码如下:

void downLoadTask()
{
    std::cout << getpid() << ": 下载任务\n"
              << std::endl;
    sleep(1);
}

void ioTask()
{
    std::cout << getpid() << ": IO任务\n"
              << std::endl;
    sleep(1);
}

void flushTask()
{
    std::cout << getpid() << ": 刷新任务\n"
              << std::endl;
    sleep(1);
}

然后我们可以对函数指针进行重名,并在main函数里面创建一个函数指针的vector对象,然后创建一个函数用于将这些函数加载进vector对象,那么这里的代码如下:

typedef void (*func_t)();
void loadTaskFunc(std::vector<func_t> *out)
{
    assert(out);
    out->push_back(downLoadTask);
    out->push_back(ioTask);
    out->push_back(flushTask);
}
int main()
{
    vector<func_t> funcMap;
    loadTaskFunc(&funcMap);
    vector<subEP> subs;
    //创建子进程
    createSubProcess(subs,funcmap);
    return 0;
}

到这里我们的子进程创建完了,管道也和进程之间连接成功了,那么接下来我们就要让父进程往管道里面发送信息。

第三步:父进程发送信息

函数创建完成之后父进程就可以给子进程发送信息,那么这里我们可以创建一个专门给子进程发送信息的函数,但是在发送信息之前得先选择一个子进程进行发送吗,因为我们要多次发送信号,所以在发送信号的时候我们得保证子进程选着的随机性和公平性,保证每个进程接收到的信号数目是差不多,所以这里我们这里采用随机数的方式来选择子进程,首先设计一个时间戳:

srand((unsigned long)time(nullptr) ^ getpid() ^ 0x171237 ^ rand() % 1234);

然后创建一个子进程的控制函数,这个函数需要三个参数:子进程的合集,函数的合集,信号的个数,那么函数的声明就如下:

void loadBlanceContrl(const std::vector<subEp> &subs, const std::vector<func_t> &funcMap, int count)
{
    
}

首先得到子进程和预处理函数的个数,然后这里我们做一个约定了,如果count的值一开始就被设计为0话就表明这里要不停的发送信号并处理函数,那么这里的代码如下:

void loadBlanceContrl(const std::vector<subEp> &subs, const std::vector<func_t> &funcMap, int count)
{
    int processnum=subs.num();
    int tasknum=funcmap.num();
    bool forver =(count==0? true:false);
    while(true)
    {

    }
}

在while循环里面我们就要获得一个随机数,然后通过模上processnum值形式来限制其范围,得到随机数之后就可以通过subs对象找到对应的子进程并获得该进程对应管道的写端,然后就可以往该管道发送数据,管道随机的同时还得保证信号的随机,所以我们还得再创建一个随机数用来保证信号的随机,然后选着进程发送信号,那么这里我们可以创建一个函数来完成发送信号的过程,该函数需要一个描述子进程的结构体和一个表示函数集的下标,那么这里的代码如下:

void sendTask(const subEp &process, int taskNum)
{
    
}
void loadBlanceContrl(const std::vector<subEp> &subs, const std::vector<func_t> &funcMap, int count)
{
    int processnum=subs.num();
    int tasknum=funcmap.num();
    bool forever =(count==0? true:false);
    while(true)
    {
        int subIdx=rand()%processnum;
        int taskIdx=rand()%tasknum;
        sendTask(subs[subIdx], taskIdx);
    }
}

那么在sendTask函数里面我们就可以打印一句话用来表示我们已经调用过该函数,然后就调用write函数往管道里面写入函数的下标,因为我们每次往管道里面就写入一个整型,write函数的返回值为写入数据的大小,所以我们这里可以拿一个参数接收write函数的返回值,并用assert函数进行一下判断,那么这里的代码如下:

void sendTask(const subEp &process, int taskNum)
{
    cout << "send task num: " << taskNum << " send to -> " << process.name_ << endl;
    int n=write(process.writeFd_,tasknum);
    assert(n==sizeof(int));
    (void)n;
}

将信号发送完之后我们就得对count的值进行一下判断,如果forever的值为fasle的话就对count的值进行减一,如果减完后count的值为0的话就用break结束循环,循环结束之后就表明信号全部发送完了,那么这时我们就可以通过for循环关闭父进程对应的写端,那么这里的代码如下:

void loadBlanceContrl(const std::vector<subEp> &subs, const std::vector<func_t> &funcMap, int count)
{
    int processnum=subs.num();
    int tasknum=funcmap.num();
    bool forever =(count==0? true:false);
    while(true)
    {
        int subIdx=rand()%processnum;
        int taskIdx=rand()%tasknum;
        sendTask(subs[subIdx], taskIdx);
    }
    if(!forever)
    {
        --count;
        if(count==0)
        {
            break;
        }
    }
    for(int i=0;i<process;i++)
    {
        close(subs[i].writeFd_);
    }
}

那么这就是信号发送的有关功能的实现。

第四步:信号的接收和实行函数

信号发送之后我们就可以创建一个函数来接收函数,在之前创建子进程的时候我们还没有将函数实现完:

void createSubProcess(std::vector<subEp> *subs, std::vector<func_t> &funcMap)
{
     for(int i=0;i<PROCSS_NUM;i++)
    {
        int fds[2]={0};
        int n=pipe(fds);
        assert(n==0);
        (void)n;
        pid_t id=fork();
        if(id==0)
        {
            //这里是子进程执行的代码
            close(fds[1]);
            while(true)
            {
                //读取管道里面的内容

                //根据管道的内容执行对应的函数
            }

        }
        //这里是父进程执行的代码
        close[fds[0]];
        subs.push_back(subEP(id,fds[1]));
    }
}

那么这里我们就可以再创建一个函数用来接收管道里面的信号,这个函数需要一个参数用来表示子进程的读端,返回值表示要执行的函数下标,那么函数的声明如下:

int recvTask(int readFd)
{
    
}

函数的开始创建一个code变量用来记录信号的值,然后使用read函数读取信号的值,read的第一个参数为对应管道的读端,第二个参数传递code变量的地址,第三个参数表示读取数据的大小那么这里就是4,因为读取的结果可能会出问题,所以我们创建一个变量用来记录该函数的返回值,如果等于4就直接返回code,如果小于等于0就返回-1其他情况就返回0,那么这里的代码如下:

int recvTask(int readFd)
{
    int code=0;
    ssize_t  s=read(fds[0],&code,sizeof(int));
    if(s==4)    {return code;}
    else if(s<=0)   {return -1;}
    else    {return 0}
}

然后在createSubProcess函数里面我们就先创建一个变量接收recvTask函数的返回值,然后进行判断如果返回值为-1的话就直接退出,其他情况就正常执行:

 while(true)
 {
     //读取管道里面的内容
     int commondCode=recvTask(fds[0]);
     //根据管道的内容执行对应的函数
     if(commondCode>=0&&commondCode<funcMap.size())
     {
         funcMap[commandCode]();
     }
     else if(commandCode == -1) 
     {
         break;
     }
 }

第五步:子进程等待函数

我们先完善一下main函数的内容:

int main()
{
    vector<func_t> funcMap;
    loadTaskFunc(&funcMap);
    vector<subEP> subs;
    //创建子进程
    createSubProcess(subs,funcmap);
    return 0;
}

首先加载函数然后创建子进程,子进程创建之后读端就已经开始等待写端信号传递过来了,那么这时我们就得控制一下子进程往里面发送信号,所以这个时候就得调用loadBlanceContrl函数发送信号:

int main()
{
    vector<func_t> funcMap;
    loadTaskFunc(&funcMap);
    vector<subEP> subs;
    //创建子进程
    createSubProcess(subs,funcmap);
    int count=10;
    loadBlanceContrl(subs,funcMap,count);
    return 0;
}

等loadBlanceContrl函数执行完成之后,读端也就已经关闭了,读端关闭了也就不会再有信号的产生,所以到这里我们还只需干一件事就是使用waitpid函数回收子进程的信息,那么我们可以再创建一个函数进行回收:

void waitProcess(std::vector<subEp> processes)
{
    int processnum = processes.size();
    for(int i = 0; i < processnum; i++)
    {
        waitpid(processes[i].subId_, nullptr, 0);
        std::cout << "wait sub process success ...: " << processes[i].subId_ << std::endl;
    }
}
int main()
{
    vector<func_t> funcMap;
    loadTaskFunc(&funcMap);
    vector<subEP> subs;
    //创建子进程
    createSubProcess(subs,funcmap);
    int count=10;
    loadBlanceContrl(subs,funcMap,count);
    waitProcess(subs);
    return 0;
}

程序的测试

PROCSS_NUM的值为3,并且count的值为10,所以当前程序运行起来之后会创建出3个子进程然后一共执行10个任务,那么运行的现象就是这样:
在这里插入图片描述
可以看到这里创建了三个子进程来处理不同的函数任务,等函数的执行个数达到10个的时候就停止了执行函数,最后对创建的三个子进程进行回收:
在这里插入图片描述

程序的bug

程序的运行符合我们的预期但是这里存在一个隐藏的bug,我们创建管道的时候是以读和写的方式来打开一个管道,后来我们会关闭父进程的读端和子进程的写段,然后子进程就可以只从管道读取数据,父进程从管道里面写入数据,当我们创建子进程的时候子进程会继承父进程的文件描述符表,可是这里会出现问题,当我们创建第一个子进程的时候父进程会指向一号管道的写段,子进程会指向1号管道的读端,当我们创建第二个子进程的时候,该子进程会继承父进程的文件描述符表,所以第二个子进程也会指向1号管道的写端,同样的道理当我们创建第三个子进程的时候,他还会继承父进程的文件描述符表,所以也会指向1号管道和2号管道的写端,又因为我们在子程序的开始过程中只会关闭进程对应管道的写段,不会管其他管道的,这就会导致当我们想要单独关闭某个主进程对管道的写端,从而结束通信时这个管道是无法通过返回0来直接关闭子进程的,所以这里依然会出现数据的损失,和子进程无法关闭的情况,那么为了解决这个问题我们就可以创建一个vector对象用来记录当前已经创建的管道,每次创建子进程时,子进程里面就一一关闭该子进程对这些管道的写段,然后在父进程里面就将新创建的管道的写段尾差到vector里面,那么这一功能的实现就放到createSubProcess函数里面,那么这里的代码如下:

void createSubProcess(std::vector<subEp> *subs, std::vector<func_t> &funcMap)
{
    std::vector<int> deleteFd;
    for(int i=0;i<PROCSS_NUM;i++)
    {
        int fds[2]={0};
        int n=pipe(fds);
        assert(n==0);
        (void)n;
        pid_t id=fork();
        if(id==0)
        {
            cout<<"创建了子进程"<< endl;
            for(int i = 0; i < deleteFd.size(); i++) close(deleteFd[i]);
            //这里是子进程执行的代码
            close(fds[1]);
            while(true)
            {
                //读取管道里面的内容
                int commondCode=recvTask(fds[0]);
                //根据管道的内容执行对应的函数
                if(commondCode>=0&&commondCode<funcMap.size())
                {
                    funcMap[commondCode]();
                }
                else if(commondCode == -1) 
                {
                    break;
                }
            }
            exit(0);
        }
        //这里是父进程执行的代码
        close(fds[0]);
        subEp sub(id,fds[1]);
        subs->push_back(sub);
        deleteFd.push_back(fds[1]);
    }
}

那么这就是本篇文章的全部内容完整的代码如下:

#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
#include <cassert>
#include <ctime>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#define PROCSS_NUM 3
using namespace std;    
typedef void (*func_t)();
class subEp // 描述子进程
{
public:
    subEp(pid_t subId, int writeFd)
        : subId_(subId), writeFd_(writeFd)
    {
        char nameBuffer[1024];
        snprintf(nameBuffer, sizeof nameBuffer, "process-%d[pid(%d)-fd(%d)]", num++, subId_, writeFd_);
        //给子进程创建名字
        name_ = nameBuffer;
    }
public:
    static int num;//用于区别每个子进程
    std::string name_;//记录子进程的名字
    pid_t subId_;//子进程的id
    int writeFd_;//子进程的写端
};
int subEp ::num =0;
void downLoadTask()
{
    std::cout << getpid() << ": 下载任务\n"
              << std::endl;
    sleep(1);
}

void ioTask()
{
    std::cout << getpid() << ": IO任务\n"
              << std::endl;
    sleep(1);
}

void flushTask()
{
    std::cout << getpid() << ": 刷新任务\n"
              << std::endl;
    sleep(1);
}
int recvTask(int readFd)
{
    int code=0;
    ssize_t  s=read(readFd,&code,sizeof(int));
    if(s==4)    {return code;}
    else if(s<=0)   {return -1;}
    else    {return 0;}
}
void createSubProcess(std::vector<subEp> *subs, std::vector<func_t> &funcMap)
{
    std::vector<int> deleteFd;
    for(int i=0;i<PROCSS_NUM;i++)
    {
        int fds[2]={0};
        int n=pipe(fds);
        assert(n==0);
        (void)n;
        pid_t id=fork();
        if(id==0)
        {
            cout<<"创建了子进程"<< endl;
            for(int i = 0; i < deleteFd.size(); i++) close(deleteFd[i]);
            //这里是子进程执行的代码
            close(fds[1]);
            while(true)
            {
                //读取管道里面的内容
                int commondCode=recvTask(fds[0]);
                //根据管道的内容执行对应的函数
                if(commondCode>=0&&commondCode<funcMap.size())
                {
                    funcMap[commondCode]();
                }
                else if(commondCode == -1) 
                {
                    break;
                }
            }
            exit(0);
        }
        //这里是父进程执行的代码
        close(fds[0]);
        subEp sub(id,fds[1]);
        subs->push_back(sub);
        deleteFd.push_back(fds[1]);
    }
}

 void loadTaskFunc(std::vector<func_t> *out)
 {
     assert(out);
     out->push_back(downLoadTask);
     out->push_back(ioTask);
     out->push_back(flushTask);
 }
void sendTask(const subEp &process, int taskNum)
{
    cout << "send task num: " << taskNum << " send to -> " << process.name_ << endl;
    int n=write(process.writeFd_,&taskNum,sizeof(taskNum));
    assert(n==sizeof(int));
    (void)n;
}
void loadBlanceContrl(const std::vector<subEp> &subs, const std::vector<func_t> &funcMap, int count)
{
    int processnum=subs.size();
    int tasknum=funcMap.size();
    bool forever =(count==0? true:false);
    while(true)
    {
        int subIdx=rand()%processnum;
        int taskIdx=rand()%tasknum;
        sendTask(subs[subIdx], taskIdx);
        sleep(1);
        if(!forever)
        {
            --count;
            if(count==0)
            {
                break;
            }
        }
    }
    for(int i=0;i<processnum;i++)
    {
        close(subs[i].writeFd_);
    }
}
void waitProcess(std::vector<subEp> processes)
{
    int processnum = processes.size();
    for(int i = 0; i < processnum; i++)
    {
        waitpid(processes[i].subId_, nullptr, 0);
        std::cout << "wait sub process success ...: " << processes[i].subId_ << std::endl;
    }
}
int main()
{
    srand((unsigned long)time(nullptr) ^ getpid() ^ 0x171237 ^ rand() % 1234);
    vector<func_t> funcMap;
    loadTaskFunc(&funcMap); 
    vector<subEp> subs;
    //创建子进程
    createSubProcess(&subs,funcMap);
    int count=10;
    loadBlanceContrl(subs,funcMap,count);
    waitProcess(subs);
    return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux安装免费Https证书,过期自动更新 省钱秘籍

Linux安装免费Https证书&#xff0c;过期自动更新 acme.sh这个服务可以友好地帮助我们获取免费的证书以及过期实现自动更新操作 官方网站&#xff1a;https://github.com/acmesh-official/acme.sh 介绍 嗨&#xff01;今天我要和大家聊聊关于使用acme.sh和Nginx结合生成HTT…

基于SpringBoot的招聘信息管理系统

基于SpringBootVue的招聘信息管理系统【附源码文档】、前后端分离 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBoot、Vue、Mybaits Plus、ELementUI工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 【主要功能】 角色&#xff1a;管理员、用户、…

运营商大数据可以帮助企业实现什么目标?

虽然现在有很多合法合规的运营商大数据&#xff0c;那么合法合规的运营商大数据可以实现什么目标&#xff1f; 1、对用户进行相应研究的目标 如果是合法合规的运营商大数据那么这种情况下&#xff0c;对于用户就能够进行相应的研究了。在这个过程中可以直接对产品的优化进行相…

【博客笔记+java+测试】

一、项目背景 1.个人博客采用前后端分离的方法来实现&#xff0c;同时使用数据库存储相关的数据&#xff0c;将其部署在云服务器上。前端主要分为五个页面&#xff1a;注册页、登录页、列表页、详情页和编辑页&#xff0c;以上模块实现了最简单的个人博客系统。其结合后端实现了…

读高性能MySQL(第4版)笔记05_优化服务器设置

1. 除非遇到异常情况&#xff0c;否则不需要调整配置 1.1. 不要“调优”服务器&#xff0c;不要使用比率、公式或“调优脚本”作为设置配置变量的基础 1.1.1. 在互联网上搜索配置建议并不总是一个好主意&#xff0c;你会在博客、论坛等找到很多糟糕的建议 1.1.2. 很难判断谁…

【高阶数据结构】红黑树 {概念及性质;红黑树的结构;带头结点的红黑树;红黑树的实现;红黑树插入操作详细解释;红黑树的验证}

红黑树 一、红黑树的概念 红黑树&#xff08;Red Black Tree&#xff09; 是一种自平衡二叉查找树&#xff0c;在每个结点上增加一个存储位表示结点的颜色&#xff0c;可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制&#xff0c;红黑树确保没有…

Python算法练习 9.11

leetcode 392 判断子序列 给定字符串 s 和 t &#xff0c;判断 s 是否为 t 的子序列。 字符串的一个子序列是原始字符串删除一些&#xff08;也可以不删除&#xff09;字符而不改变剩余字符相对位置形成的新字符串。&#xff08;例如&#xff0c;"ace"是"abcd…

波奇学C++:多态知识点

多态中函数的重写&#xff08;基类指针访问派生类函数&#xff09;&#xff0c;只重写函数的实现&#xff0c;而不重写声明。 class Person { public:virtual void fun(int i 0){cout << "Person"<<" "<<i;} }; class Student:public …

H5页面safari浏览器底部遮挡问题解决方案亲测有效

media screen and (max-width: 767px) { _::-webkit-full-page-media, _:future, :root .safari_only { padding-bottom: 120px; //解决Safari浏览器底部遮挡问题 } } 然后给对应div加上这个类名就可以了

本地录像视频文件如何推送到视频监控平台EasyCVR进行AI视频智能分析?

安防监控平台EasyCVR支持多协议、多类型设备接入&#xff0c;可以实现多现场的前端摄像头等设备统一集中接入与视频汇聚管理&#xff0c;并能进行视频高清监控、录像、云存储与磁盘阵列存储、检索与回放、级联共享等视频功能。视频汇聚平台既具备传统安防监控、视频监控的视频能…

ESP32开发:Clion配置IDF

IDF环境搭建 使用安装包安装IDF 可以通过安装包进行安装&#xff0c;如下图&#xff1a; 下载链接如下&#xff1a;https://dl.espressif.cn/dl/esp-idf/?idf4.4 安装好后&#xff0c;IDF会添加环境变量IDF_TOOLS_PATH&#xff0c;如果要安装多个IDF&#xff0c;为了防止冲…

Java笔记:ThreadLocal

1. ThreadLocal简介 多线程访问同一个共享变量的时候容易出现并发问题&#xff0c;特别是多个线程对一个变量进行写入的时候&#xff0c;为了保证线程安全&#xff0c;一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步…

031:vue子组件向父组件传递多个参数,父组件2种解析方法

第031个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下&#xff0c;本专栏提供行之有效的源代码示例和信息点介绍&#xff0c;做到灵活运用。 &#xff08;1&#xff09;提供vue2的一些基本操作&#xff1a;安装、引用&#xff0c;模板使…

6个超好用的视频素材网站,4K/8K高质量,免费下载。

很多视频剪辑和做自媒体的朋友都不知道去哪里找视频素材&#xff0c;而且很多网站的素材可以免费下载但是不能商用&#xff0c;还有需要付费购买使用。下面推荐几个良心网站&#xff0c;视频素材免费下载&#xff0c;还能商用&#xff0c;赶紧收藏起来吧。 1、菜鸟图库 https:…

基于Python和mysql开发的在线音乐网站系统(源码+数据库+程序配置说明书+程序使用说明书)

一、项目简介 本项目是一套基于Python和mysql开发的在线音乐网站系统(&#xff0c;主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的Python学习者。 包含&#xff1a;项目源码、项目文档、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经…

qt 正则表达式

以上是正则表达式的格式说明 以下是自己写的正则表达式 22-25行 是一种设置正则表达式的方式&#xff0c; 29-34行 : 29行 new一个正则表达式的过滤器对象 30行 正则表达式 的过滤格式 这个格式是0-321的任意数字都可以输入 31行 将过滤格式保存到过滤器对象里面 32行 将验…

VSCode搭建Django开发环境

文章目录 一、Django二、搭建步骤1. 安装python和VSCode&#xff0c;安装插件2. VSCode打开项目文件夹3. 终端中键入命令&#xff1a;建立虚拟环境4. 选择Python的解释器路径为虚拟环境5. 在虚拟环境中安装Django6.创建Django项目7. 创建app应用8. 运行应用9. 修改配置中文显示…

JVM 虚拟机 ----> Java 类加载机制

文章目录 JVM 虚拟机 ----> Java 类加载机制一、概述二、类的生命周期1、类加载过程&#xff08;Loading&#xff09;&#xff08;1&#xff09;加载&#xff08;2&#xff09;验证&#xff08;3&#xff09;准备&#xff08;4&#xff09;解析&#xff08;5&#xff09;初始…

纯小白安卓刷机1

文章目录 常见的英文意思刷机是什么&#xff1f;为什么要刷机&#xff1f;什么是BL锁&#xff08;BootLoader锁&#xff09;&#xff1f;我的机能够刷机吗&#xff1f;什么是Boot镜像/分区&#xff1f;什么是Recovery镜像/分区&#xff08;缩写为rec&#xff09;&#xff1f;什…

2023-2024年最值得选的Java毕业设计选题大全:2000个热门选题推荐

一、前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; 毕业设计选题非常重要&a…