『 Linux 』匿名管道应用 - 简易进程池

news2024/11/15 11:56:05

文章目录

    • 池化技术
    • 进程池
    • 框架及基本思路
    • 进程的描述
    • 组织
      • 管道通信建立的潜在问题
    • 任务的描述与组织
    • 子进程读取管道信息
    • 控制子进程
    • 进程退出及资源回收


池化技术

请添加图片描述

池化技术是一种编程技巧,一般用于优化资源的分配与复用;

当一种资源需要被使用时这意味着这个资源可能会被进行多次使用或者需要同时使用多个该资源,当出现这种情况时内核将会频繁的对该资源进行申请并释放,大大降低整体的效率;

池化技术旨在预先分配一组资源,当用户层需要使用这些资源时将直接对预先分配资源进行使用;

若是预先分配资源不足以当前使用情况时将再次申请一批,动态增长的资源在使用过后将被释放以保证不出现资源浪费情况;

所用资源的数量始终>=预先分配的资源数量;

实现以下几点:

  • 减少资源申请及释放
  • 提高资源使用效率
  • 资源数量控制
  • 资源动态拓展

常见的池化技术有如下:

  • 进程池

    适用于大量进程执行短期任务的情况;

  • 内存池

    预先分配一大块内存,然后在这块内存当中划分出多个小块内存用于动态分配与回收;

  • 线程池

    适用于任务处理不需要大量资源单需要大量并发执行情况;


进程池

请添加图片描述

进程池是一种用于并发执行的资源池技术;

预先创建一定数量的进程用于执行提交给进程池的任务;

这些进程在池中保持活跃状态并可以快速响应并执行任务而不需要每次任务到来时再创建新的进程从而提高整体工作效率;

进程池通常用于以下几种情况:

  • 性能提高

    进程的创建与销毁具有开销,尤其在高负载或多任务并发的场景中使用进程池可以避免频繁的 创建/销毁 进程从而提高系统性能;

  • 资源限制

    限制进程池的大小可以避免系统资源(CPU,内存等)被过度消耗;

  • 负载均衡

    进程池可以通过系统负载情况动态分配任务,使各个进程的工作量保持均衡;

本文模拟实现的进程池通过多个匿名管道实现进程间通信使得一个进程与多个其对应的血缘关系进程进行协同从而形成一个进程池[父写子读];


框架及基本思路

请添加图片描述

  • 创建文件

    • ProcessPool.cc

      本程序不采用声明与定义分离的思路,该文件用于main函数以及对应的接口函数的声明及其实现定义;

    • Task.hpp

      用于设计需要投喂给进程池的任务列表;

整体构造采用先描述后组织的方式对进程池进行设置,并以自顶向下的方式进行设计,即先将所需接口以声明的形式标出而后再对接口进行具体实现;

总体为:


#define PROCESSNUM 5  // 控制子进程创建个数
/* 设计为默认预分配5个进程 */

/*
	...描述
*/

int main() {
// 初始化任务列表
    LoadTask(&tasks);
    
	// 组织    
    // 0.以数据结构的形式将进程进行组织
    std::vector<channel> channels;
    // 1.初始化进程池
    InitProcessPool(&channels);
    // 2.控制子进程
    ControlSlaver(channels);
    // 3.清理收尾
    QuitProcess(channels);
    return 0;
}

基本思路为父进程调用pipe()系统调用接口创建管道文件;

再根据进程池的进程数量通过循环调用fork()系统调用接口创建子进程,再根据数据流向调用close()系统调用接口关闭不需要的文件描述符使得父进程能与每个创建的子进程利用管道文件相连接从而构成单向管道通信信道;

当子进程与对应的通信信道被建立后父进程根据描述将子进程以数据结构的方式进行管理;

父进程继续执行后面的代码用于对子进程发送任务,子进程通过循环使其保持活跃状态并等待父进程向管道发送任务并对任务进行处理;

当识别到对应的退出指令后父进程对进程池进行清理同时调用wait()系统调用接口等待并回收子进程;


进程的描述

请添加图片描述

一个进程再被组织与管理前需要被进行描述;

基本的信息为:

  • 发送任务用的文件描述符
  • 进程的PID
  • 进程名
#include "Task.hpp"

#include <unistd.h>

#include <cassert>

#include <string>

#include <vector>

#include <cstdlib>

#include <ctime>

#include <sys/wait.h>

#include <sys/stat.h>


#define PROCESSNUM 5  // 控制子进程创建个数

// 描述
class channel {
 public:
  // 构造函数
  channel(int cmdfd, pid_t slaveryid, const std::string& processname)
      : _cmdfd(cmdfd), _slaveryid(slaveryid), _processname(processname) {}

 public:
  int _cmdfd;                // 发送任务用文件描述符
  pid_t _slaveryid;          // 进程的pid
  std::string _processname;  // 进程名
};

int main() {
    // 初始化任务列表
    LoadTask(&tasks);
    // 组织
    std::vector<channel> channels;
    InitProcessPool(&channels);

    // 2.控制子进程
    ControlSlaver(channels);
    // 3.清理收尾
    // sleep(1000);
    QuitProcess(channels);
    return 0;
}

组织

请添加图片描述

组织的方式为现将所需的进程调用fork()系统调用接口再将用数据结构将其进行组织以方便后期控制子进程以及管理子进程;

void InitProcessPool(std::vector<channel>* channels) {  
    // 1.初始化
        pid_t id = fork();
        if (id < 0) {
            // 进程创建失败
            std::cerr << "fork errno" << std::endl;
            assert(id >= 0);  // 差错处理 子进程创建失败
        }
        if (id == 0) {
            // 子进程
            close(pipefd[1]);
            // 子进程读 关闭[1]
            /*
                    slaver(pipefd[0]);
                这是一种做法 为从这个描述符当中读取任务数据并执行任务
            */
            dup2(pipefd[0], 0);
            // 该种做法为 使用dup2接口进行重定向
            // 使得子进程的默认输入从键盘改为pipefd[0]中读取
            slaver();//默认从文件描述符0 获取任务信息即可
            std::cout << "Process : " << getpid() << " quit sucess" << std::endl;
            exit(0);
    }
    // 父进程
    close(pipefd[0]);  // 父进程关闭读端

    // 添加字段
    std::string name = "Process" + std::to_string(i);

    channels->push_back(channel(pipefd[1], id, name));  // 调用构造函数进行初始化

    }
}

int main(){
    //...
    std::vector<channel> channels;
    InitProcessPool(&channels);

    //...
    return 0;
}

子进程创建后采用数据结构进行管理;

本文中实现的进程池对于管道数据流向为 父写子读 ;

使用if()条件判断区别父子进程,父进程在执行完对应的代码后将自己部分的该函数的栈帧进行销毁;

而子进程将调用slaver()函数从对应的文件描述符中读取父进程写进管道中的数据及任务;

对于父进程而言其管理着channel数组,数组中存放着所有当前子进程的所有信息;

debug来尝试查看对应的channel数组中所存储的信息;

/* 在main函数中进行调用 */
void Debug(const std::vector<channel>& channels) {
    for (auto& c : channels) {
    std::cout << "cmdfd : " << c._cmdfd << std::endl
              << "slaveryid : " << c._slaveryid << std::endl
              << "processname : " << c._processname << std::endl;

    std::cout << "---------------------" << std::endl;
  }
}

debug后的结果为:

cmdfd : 4
slaveryid : 9619
processname : Process0
---------------------
cmdfd : 5
slaveryid : 9620
processname : Process1
---------------------
cmdfd : 6
slaveryid : 9621
processname : Process2
---------------------
cmdfd : 7
slaveryid : 9622
processname : Process3
---------------------
cmdfd : 8
slaveryid : 9623
processname : Process4
---------------------

结果中父进程将通过文件描述符 4 ~ 8 向各个子进程发送数据,这些文件描述符是管道的写端;

子进程将从文件描述符 3 读取数据;

slaver()为子进程读取对应文件描述符,其需要传入一个参数为文件描述符fd;

子进程读取管道数据的方式有两种:

  • 从文件描述符3中读取

    这个数组是一个临时的空间,存放着管道文件的读写文件描述符,需要父子进程根据数据流向来关闭另外一个不需要的文件描述符;

    本文中的文件描述符中父进程为写方,需要关闭文件描述符pipefd[0]即读端,而子进程需要关闭写端,但其可以从文件描述符3也就是pipefd[1]中读取父进程写入管道的数据,但需在调用slaver()函数时传入对应的文件描述符

    即调用slaver(pipefd[1])即可;

  • 从文件描述符0中读取

    文件描述符0一般作为标准输入,默认从键盘等文件进行读取;

    即调用dup2()系统调用接口将文件描述符3重定向至文件描述符0,以减少调用slaver()函数的传参步骤,变相减少程序的可维护成本;


管道通信建立的潜在问题

请添加图片描述

在组织的初始化中存在一个问题,即子进程将冗余存在多个写端;

子进程为父进程的一份拷贝,当父子进程中其中一个进程被修改时(即对物理内存进行修改);

为了避免一个进程的写入操作影响到另一个进程,将会发生写时拷贝操作(不是本节重点);

管道文件是一种内存级文件,没有实质的Inode与对应的 数据块 ;

但是其写入与读取的操作是根据文件描述符进行的,在进行fork()系统调用接口创建子进程时文件描述符也会被拷贝一份,这造成了子进程中存在着冗余的写端(继承其父进程的写端);

本文设计的进程池为退出时父进程关闭写端从而使子进程读端读取失败即读取到文件末尾并返回0后逐步回收子进程;

但出现文件描述符冗余情况时父进程关闭写端时依旧可能有其他子进程指向该位置的写端从而导致无法正常将程序退出;

解决办法有两种:

  • 倒序关闭文件描述符

    int main(){
        
        // 初始化任务列表...
        // 组织...
        // 2.控制子进程...
        // 3.清理收尾...
        
        int last = channels.size()-1;
        for (; last >= 0;--last){
            close(channels[last]._cmdfd);
            sleep(3);
            waitpid(channels[last]._slaveryid, nullptr, 0);
            sleep(3);
        }
        /* 
            采用倒序关闭文件描述符的方法确实可以确保在结束时逐一关闭每个子进程对应的写端
            这会导致子进程读端在读到文件结尾(EOF)时退出循环
            并且子进程会调用 exit(0)
            进入僵尸状态等待父进程回收
        */
        for(const auto &c:channels ){
            close(c._cmdfd);
            }ss
        sleep(5);
        for (const auto& c : channels) {
            waitpid(c._slaveryid, nullptr, 0);
        }
        sleep(5);
        return 0;
    }
    

    在父进程发出退出指令时倒序遍历channel数组,父进程依次关闭对应的写端,当关闭最后一个进程的写端时其读端将会读取到0,而后根据程序设计对资源进行回收;

    当该子进程退出时其指向上一个管道文件的写端指向将消失,而后依次进行回收;

    该方法可以使得程序正常退出,但无法解决实质性问题即父子进程中建立的为单向通信信道,在程序运行中不符合管道的单向信道规范;

  • 在初始化中关闭多余的文件描述符

    在初始化中关闭文件描述符可以保证程序在运行当中可以存在正确的单向通信规范;

    具体操作为父进程采用数据结构保留对应的写端文件描述符,并在下次fork()创建子进程时在子进程中遍历数组从而能够关闭对应的文件描述符;

    void InitProcessPool(std::vector<channel>* channels) {  // 1.初始化
    
        //version 2 -- 确保每个子进程只有一个写端
        std::vector<int> oldfds;
        for (int i = 0; i < PROCESSNUM; ++i) {
            // 子进程创建时先创建管道
            int pipefd[2];
            int n = pipe(pipefd);
            assert(!n);  // 差错处理 pipe创建管道失败
            (void)n;
    
    
            pid_t id = fork();
            if (id < 0) {
                // 进程创建失败
                std::cerr << "fork errno" << std::endl;
                assert(id >= 0);  // 差错处理 子进程创建失败
            }
            if (id == 0) {
                // 子进程
                close(pipefd[1]);
                for(const auto& fd:oldfds){
                    close(fd);
                    /*
                        这里的close不会调用失败
                        在下文中的oldfds数组将会记录父进程对上一个子进程的写端 
                        所以会导致子进程会对其他子进程存在写端
                        所以这里的close并不会调用失败 
                        因为对于每个子进程而言 数组中的所有文件描述符都是有效的文件描述符
                    */
                }
                // 子进程读 关闭[1]
                /*
                        slaver(pipefd[0]);
                    这是一种做法 为从这个描述符当中读取任务数据并执行任务
                */
                dup2(pipefd[0], 0);
                // 该种做法为 使用dup2接口进行重定向
                // 使得子进程的默认输入从键盘改为pipefd[0]中读取
                slaver();//默认从文件描述符0 获取任务信息即可
                std::cout << "Process : " << getpid() << " quit sucess" << std::endl;
                exit(0);
        }
        // 父进程
        close(pipefd[0]);  // 父进程关闭读端
    
        // 添加字段
        std::string name = "Process" + std::to_string(i);
    
        channels->push_back(channel(pipefd[1], id, name));  // 进行初初始化
    
        oldfds.push_back(pipefd[1]);
    
        // Debug
        // std::cout << "==============================" << std::endl;
        // std::cout << "对oldfds数组进行打印 [" << debugn <<"]@ "<< std::endl;
        // debugn++;
        // for (const auto& fd : oldfds) {
        //   std::cout << fd << std::endl;
        // }
    
        // std::cout << "==============================" << std::endl;
        }
    }
    

任务的描述与组织

请添加图片描述

任务的描述与组织在Task.hpp文件当中;

思路为以数据结构和函数指针相配合的方式将函数(任务)以加载的方式进行管理(在main函数所在文件中需要注意使用extern声明);

	/* ######## */
	/* Task.hpp */
	/* ######## */

#pragma once

#include <iostream>

#include <vector>

typedef void (*task_t)(); //    定义函数指针

void task1() { 
    std::cout << "任务1 : 数据初始化(Data Initialization)" << std::endl;
}
void task2(){
    std::cout << "任务2 : 更新数据(Update Data)" << std::endl;
}
void task3(){
    std::cout << "任务3 : 获取数据(Retrieve Data)" << std::endl;
}
void task4(){
    std::cout << "任务4 : 数据验证(Data Validation)" << std::endl;
}
void task5(){
    std::cout << "任务5 : 生成报告(Generate Report)" << std::endl;
}
void task6(){
    std::cout << "任务6 : 备份数据(Backup Data)" << std::endl;
}


void LoadTask(std::vector<task_t> *tasks){
    tasks->push_back(task1);
    tasks->push_back(task2);
    tasks->push_back(task3);
    tasks->push_back(task4);
    tasks->push_back(task5);
    tasks->push_back(task6);
}

该处task数组中的下标即为任务码;

通过数组的形式访问函数指针从而调用对应函数;


子进程读取管道信息

请添加图片描述

void slaver() {
  // 子进程读取父进程向管道内写入的信息
  while (true) {
    /*
        debug
        //每个子进程都将从文件描述符rfd中进行读取
        在当前程序中 rfd为3
        重定向后为0
    // std::cout << getpid() << " - read fd is : " << rfd << std::endl;
    */

    int cmdcode = 0;
    int n = read(0,&cmdcode,sizeof(int));
    if(n == sizeof(int)){
        //执行cmdcode对应的任务列表
        std::cout <<"slaver get a cmdcode @["<< getpid() << "] : cmdcode : " << cmdcode << std :: endl;
        if (cmdcode < 0 || cmdcode > (int)tasks.size()) continue;
        tasks[cmdcode]();//指针数组
        std::cout << "--------------------------" << std::endl;
    }
    if(n == 0)//说明写端被关闭 0表示文件结尾
        break;
    // sleep(100);
  }
}

以循环的方式通过调用read()系统调用接口分别向各自管道内读取数据,若是管道不存在数据则进行等待(默认行为),意味着子进程不需要调用sleep()等接口来进行时间的延长;

子进程读取到对应信息时将对数据进行分析并进行处理(根据需求,当前简易进程池中不需要对数据进行分析);

当读取到0时说明读取到文件末尾,即写端被关闭,此时子进程退出并进入僵尸状态等待被父进程回收;


控制子进程

请添加图片描述

子进程可以读取父进程写入管道文件的数据时父进程可以根据需求将对应的任务码通过write()系统调用接口写入管道文件当中并等待子进程读取处理;

该处为三步操作:

  • 选择任务

    任务即为tasks函数指针数组中所存储的各个函数指针;

    通过下标的方式进行选择,其中下标即为任务对应的任务码;

  • 选择进程

    为避免多个任务在同一个进程下进行等待而降低效率,进程的选择需要依靠负载均衡;

    常见的负载均衡包括:

    • 随机

      通过伪随机或是真随机的方式为各个子进程分配任务;

    • 轮询/轮转

      通过轮询/轮转的方式时多个子进程为一个周期轮流分配任务;

    • 最少连接

      选择当前连接数(需处理数据数量)最少的进程优先为其分配任务;

  • 发送任务(任务码)

    调用write()系统调用接口将任务码和所需数据写入至管道文件中;

void ControlSlaver(const std::vector<channel> &channels){
//向子进程派发任务
    //需要随机
    Menu();
    for (int i = 1; i <= 100; ++i) {
    //(1) 选择任务
    // int cmdcode = rand() % tasks.size();
    std::cout << "Please Enter your choic :";
    int choice = 0;
    std::cin >> choice;
    if(choice==0){
      std::cout << "正在退出" << std::endl;
      return;
    }
    choice -= 1;
    //(2) 选择进程 -- 需要负载均衡 (随机数或是轮询 此处使用随机)
    int fd = rand() % PROCESSNUM;
    //(3) 发送任务 (任务码)
    std::cout << "Parent Process say : " << std::endl
                << "cmdcode = " << choice << " alread sendto "
                << channels[fd]._processname << channels[fd]._slaveryid
                << std::endl;
    write(channels[fd]._cmdfd, &choice, sizeof(choice));
    sleep(1);
    }
}

需要时可在控制接口中打印菜单:

void Menu(){
    std::cout << "#############################################\n";
    std::cout << "#                 主菜单                    #\n";
    std::cout << "#############################################\n";
    std::cout << "# 1. 数据初始化                             #\n";
    std::cout << "# 2. 更新数据                               #\n";
    std::cout << "# 3. 获取数据                               #\n";
    std::cout << "# 4. 数据验证                               #\n";
    std::cout << "# 5. 生成报告                               #\n";
    std::cout << "# 6. 备份数据                               #\n";
    std::cout << "# 0. 退出                                   #\n";
    std::cout << "#############################################\n";
}

进程退出及资源回收

请添加图片描述

两种回收方式:

  • 倒序关闭文件描述符

    参考上文,此处不赘述;

  • 正常退出回收

    正常回收的情况下父子进程间的匿名管道必须是单向信道;

    即当进程池的一批子进程被创建完毕后应及时对冗余的写端(子进程的)进行关闭;

    即可正常回收;

退出与回收即为父进程遍历对应的channels数组,将对应的写端(文件描述符)进行关闭;

当关闭文件描述符时读端(子进程)的read()系统调用接口将默认读取到0表示读取到了文件结束符号,在子进程的slaver()接口中根据依次判断是否读取到0来依次退出子进程的循环,使得子进程正常退出进入僵尸状态;

父进程则调用waitpid()系统调用接口轮流回收已经进入僵尸状态的子进程;

void QuitProcess(const std::vector<channel> channels)
{

    // 正常回收 - 对应的需要在一批子进程被创建完毕后应及时对冗余的写端(子进程的)进行关闭
    for(const auto &c:channels ){
        close(c._cmdfd);
        waitpid(c._slaveryid, nullptr, 0);
    }

    // // version 1 - 倒序关闭文件描述符回收法
    // int last = channels.size()-1;
    // for (; last >= 0;--last){
    //     close(channels[last]._cmdfd);
    //     sleep(3);
    //     waitpid(channels[last]._slaveryid, nullptr, 0);
    //     sleep(3);
    // }
    // /* 
    //     采用倒序关闭文件描述符的方法确实可以确保在结束时逐一关闭每个子进程对应的写端
    //     这会导致子进程读端在读到文件结尾(EOF)时退出循环
    //     并且子进程会调用 exit(0)
    //     进入僵尸状态等待父进程回收
    // */

    // for(const auto &c:channels ){
    //     close(c._cmdfd);
    //     }ss
    // sleep(5);
    // for (const auto& c : channels) {
    //     waitpid(c._slaveryid, nullptr, 0);
    // }
    // sleep(5);
}

  • 完整代码(供参考):
    [参考代码(gitee) - DIo夹心小面包 (半介莽夫)]

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

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

相关文章

GuLi商城-商品服务-API-品牌管理-JSR303分组校验

注解:@Validated 实体类: package com.nanjing.gulimall.product.entity;import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.nanjing.common.valid.ListValue; import com.nanjing.common.valid.Updat…

代码随想录——不同路径Ⅱ(Leetcode 63)

题目链接 动态规划 class Solution {public int uniquePathsWithObstacles(int[][] obstacleGrid) {int m obstacleGrid.length;int n obstacleGrid[0].length;int[][] dp new int[m][n];// 遇到障碍则从(0,0)到达for(int i 0; i < m && obstacleGrid[i][0] …

【C++】初始化列表”存在的意义“和“与构造函数体内定义的区别“

构造函数是为了方便类的初始化而存在&#xff0c;而初始化时会遇到const成员变量、引用成员变量等&#xff0c;这些变量不允许函数内赋值&#xff0c;必须要在初始化时进行赋值&#xff0c;所以就有了初始化列表&#xff0c;初始化列表只能存在于类的构造函数中&#xff0c;用于…

百日筑基第二十天-一头扎进消息队列3-RabbitMQ

百日筑基第二十天-一头扎进消息队列3-RabbitMQ 如上图所示&#xff0c;RabbitMQ 由 Producer、Broker、Consumer 三个大模块组成。生产者将数据发送到 Broker&#xff0c;Broker 接收到数据后&#xff0c;将数据存储到对应的 Queue 里面&#xff0c;消费者从不同的 Queue 消费数…

PySide(PyQt),csv文件的显示

1、正常显示csv文件 import sys import csv from PySide6.QtWidgets import QApplication, QMainWindow, QTableWidget, QTableWidgetItem, QWidgetclass CSVTableWidgetDemo(QMainWindow):def __init__(self):super().__init__()# 创建显示控件self.widget QWidget(self)sel…

Hadoop-28 ZooKeeper集群 ZNode简介概念和测试 数据结构与监听机制 持久性节点 持久顺序节点 事务ID Watcher机制

章节内容 上节我们完成了&#xff1a; ZooKeeper 集群配置ZooKeeper 集群启动ZooKeeper 集群状况查看Follower 和 Leader 节点 背景介绍 这里是三台公网云服务器&#xff0c;每台 2C4G&#xff0c;搭建一个Hadoop的学习环境&#xff0c;供我学习。 之前已经在 VM 虚拟机上搭…

html表格账号密码备忘录:表格内容将通过JavaScript动态生成。点击查看密码10秒关闭

<!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8"><title>账号密码备忘录</title><style>body {background: #2c3e50;text-shadow: 1px 1px 1px #100000;}/* 首页样式开始 */.home_page {color: …

Qt:22.鼠标相关事件(实例演示——鼠标进入/离开某控件的事件、鼠标按下事件、鼠标释放事件、鼠标双击事件)

目录 1.实例演示——鼠标进入/离开某控件的事件&#xff1a; 2.鼠标按下事件&#xff1a; 3.鼠标释放事件&#xff1a; 4.鼠标双击事件&#xff1a; 1.实例演示——鼠标进入/离开某控件的事件&#xff1a; 首先创建一个C类文件 Label&#xff0c;填写好要继承的父类 QLabe…

【ARM】使用JasperGold和Cadence IFV科普

#工作记录# 原本希望使用CCI自带的验证脚本来验证修改过后的address map decoder&#xff0c;但是发现需要使用JasperGold或者Cadence家的IFV的工具&#xff0c;我们公司没有&#xff0c;只能搜搜资料做一下科普了解&#xff0c;希望以后能用到吧。这个虽然跟ARM没啥关系不过在…

HarmonyOS NEXT:一次开发,多端部署

寄语 这几年特别火的uni-app实现了“一次开发&#xff0c;多端使用”&#xff0c;它这个端指的是ios、安卓、各种小程序这些&#xff0c;而HarmonyOS NEXT也提出了“一次开发&#xff0c;多端部署”&#xff0c;而它这个端指的是终端设备&#xff0c;也就是我们的手机、平板、电…

基于大语言模型(LLM)的合成数据生成、策展和评估的综述

节前&#xff0c;我们星球组织了一场算法岗技术&面试讨论会&#xff0c;邀请了一些互联网大厂朋友、参加社招和校招面试的同学。 针对算法岗技术趋势、大模型落地项目经验分享、新手如何入门算法岗、该如何准备、面试常考点分享等热门话题进行了深入的讨论。 合集&#x…

【Mongodb-04】Mongodb聚合管道操作基本功能

Mongodb系列整体栏目 内容链接地址【一】Mongodb亿级数据性能测试和压测https://zhenghuisheng.blog.csdn.net/article/details/139505973【二】springboot整合Mongodb(详解)https://zhenghuisheng.blog.csdn.net/article/details/139704356【三】亿级数据从mysql迁移到mongodb…

【Mac】App Cleaner Uninstaller(Mac应用清理和卸载)及同类型软件介绍

今天给大家介绍的软件是App Cleaner & Uninstaller&#xff0c;这是一款mac应用清理和卸载软件&#xff0c;还会介绍同类型的其他几款软件&#xff0c;大家可以选择自己适合的来使用。 App Cleaner & Uninstaller软件介绍 App Cleaner & Uninstaller 是一款专门用…

《知识点扫盲 · 学会 WebService》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…

《斯科特·凯尔比的风光摄影手册》读书笔记

写在前面 《斯科特凯尔比的风光摄影手册》读书笔记整理没有全部读完&#xff0c;选择了感兴趣的章节理解不足小伙伴帮忙指正 &#x1f603;,生活加油 99%的焦虑都来自于虚度时间和没有好好做事&#xff0c;所以唯一的解决办法就是行动起来&#xff0c;认真做完事情&#xff0c;…

NLP之词的重要性

文章目录 何为重要词TF*IDFTF*IDF其他版本TFIDF 算法特点TF*IDF的优势TF*IDF劣势 TF*IDF的应用搜索引擎文本摘要文本相似度计算 上一篇文章介绍了新词的发现&#xff0c;用内部凝固度和左右熵来发现新词。这时候机器对一篇文章有了对词的一定理解&#xff0c;这时我们让机器上升…

云服务器重置密码后,xshell远程连接不上,重新启用密码登录方式

云服务器重置密码后 &#xff0c;xshell连接出现不能使用密码登录 解决方案&#xff1a;以下来自阿里云重新启用密码登录方式帮助文档 为轻量应用服务器创建密钥且重启服务器使密钥生效后&#xff0c;服务器会自动禁止使用root用户及密码登录。如果您需要重新启用密码登录方式&…

【python】基于决策树的语音识别

目录 引言 决策树的主要特点 决策树的构建过程 决策树的应用 数据集 代码实现 引言 决策树&#xff08;Decision Tree&#xff09;是一种常用的分类与回归方法&#xff0c;其中最为人所知的是其在分类问题上的应用。决策树模型呈树形结构&#xff0c;其中每个内部节点表…

centos7|Linux操作系统|编译最新的OpenSSL-3.3,制作rpm安装包

一、 为什么需要编译rpm包 通常&#xff0c;我们需要安装某个软件&#xff0c;尤其是在centos7这样的操作系统&#xff0c;一般是通过yum包管理器来安装软件&#xff0c;yum的作用是管理rpm包的依赖关系&#xff0c;自动的处理rpm包的安装顺序&#xff0c;安装依赖等的相关问…

【数智化案例展】沃太能源——MES系统建设引领智能制造新篇章

‍ 联想集团案例 本项目案例由联想集团投递并参与数据猿与上海大数据联盟联合推出的《2024中国数智化转型升级创新服务企业》榜单/奖项评选。 大数据产业创新服务媒体 ——聚焦数据 改变商业 沃太能源股份有限公司&#xff0c;一家在储能产品及智慧能源管理方案领域享有盛誉的…