【进程间通信:管道】

news2024/11/22 16:51:15

目录

1 进程间通信介绍

1.1 进程间通信目的

1.2 进程间通信发展  

1.3 进程间通信分类

 2 管道

2.1 什么是管道

2.2 匿名管道

2.2.1 匿名管道的使用

 2.2.2 使用匿名管道创建进程池

2.3 管道读写规则

2.4 匿名管道特点

2.5 命名管道

2.5.1 概念

2.5.2 使用


1 进程间通信介绍

1.1 进程间通信目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程 。
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

1.2 进程间通信发展  

  • 管道
  • System V进程间通信
  • POSIX进程间通信

1.3 进程间通信分类

管道

  • 匿名管道pipe
  • 命名管道
System V IPC
  • System V 消息队列
  • System V 共享内存
  • System V 信号量
POSIX IPC
  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

 2 管道

2.1 什么是管道

管道是 Unix 中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个 管道".

比如我们常见的命令 | , 我们知道其实我们执行的命令在linux上本质是执行一个进程,管道也分为匿名管道和命名管道,像上面这种没有名字的就叫做匿名管道。

2.2 匿名管道

2.2.1 匿名管道的使用

在文件描述符得时候我们讲过,子进程会继承父进程的文件描述符,但是子进程并不会去拷贝父进程的文件,也就是子进程与父进程其实看到的是同一份文件,这就具备了进程间通信的前提:两个进程看到了同一份资源。我们就能够根据我们的需求来让父子进程完成我们的任务(比如让父进程写文件,子进程从文件中读取)

#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

我们可以用pipe函数来帮助我们创建匿名管道(大家一定要注意,使用匿名管道的前提是在父进程创建子进程前就已经把管道打开了,这样子进程才能够继承父进程的文件描述符)

 实例代码:

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<cerrno>
#include<cstring>
#include<string>
using namespace std;

int main()
{
    
    int pipefd[2]={0};
    int n=pipe(pipefd);
    if(n<0)
    {
        cout<<"error"<<":"<<strerror(errno)<<endl;
        return 1;
    }

    pid_t id=fork();
    if(id==0)
    {
        //child 子进程读取,父进程写入
        close(pipefd[1]);
        char buffer[1024];
        while(true)
        {
            int n=read(pipefd[0],buffer,9);
            if(n>0)
            {
                buffer[n]='\0';
                cout<<"child :"<<buffer<<endl;
            }
            else if(n==0)
            {
                cout<<"read file end"<<endl;
            }
            else 
            {
                cout<<"read error"<<endl;
            }
        }

        close(pipefd[0]);
        exit(0);
    }

    //parent 子进程读取,父进程写入
    close(pipefd[0]);
    const char* str="hello bit";
    while(true)
    {
        write(pipefd[1],str,strlen(str));
    }
    close(pipefd[1]);

    int status=0;
    waitpid(id,&status,0);
    cout<<"singal:"<<(status&0x7f)<<endl;
    return 0;
}

这样我们就编写完成了一份基本的用匿名管道进行通信的方法。

代码中值得注意的细节有:

  1. 系统规定数组下标为0表示读端,数组下标为1表示写端
  2. 父子进程一个完成写入,一个完成读取,在写入前应当关闭读端,同理在读取前应当关闭写段。

 2.2.2 使用匿名管道创建进程池

我们可以用一个匿名管道来做一些比较优雅的事情:比如创建一个进程池,用一个父进程管理多个子进程:

contralProcess.cc:

#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include "Task.hpp"
using namespace std;

const int gnum = 3;
Task t;

class EndPoint
{
private:
    static int number;
public:
    pid_t _child_id;
    int _write_fd;
    std::string processname;
public:
    EndPoint(int id, int fd) : _child_id(id), _write_fd(fd)
    {
        //process-0[pid:fd]
        char namebuffer[64];
        snprintf(namebuffer, sizeof(namebuffer), "process-%d[%d:%d]", number++, _child_id, _write_fd);
        processname = namebuffer;
    }
    std::string name() const
    {
        return processname;
    }
    ~EndPoint()
    {
    }
};

int EndPoint::number = 0;

// 子进程要执行的方法
void WaitCommand()
{
    while (true)
    {
        int command = 0;
        int n = read(0, &command, sizeof(int));
        if (n == sizeof(int))
        {
            t.Execute(command);
        }
        else if (n == 0)
        {
            std::cout << "父进程让我退出,我就退出了: " << getpid() << std::endl; 
            break;
        }
        else
        {
            break;
        }
    }
}

void createProcesses(vector<EndPoint> *end_points)
{
    vector<int> fds;
    for (int i = 0; i < gnum; i++)
    {
        // 1.1 创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;

        // 1.2 创建进程
        pid_t id = fork();
        assert(id != -1);
        // 一定是子进程
        if (id == 0)
        {
            for(auto &fd : fds) close(fd);

            
            // 1.3 关闭不要的fd
            close(pipefd[1]);
            // 我们期望,所有的子进程读取"指令"的时候,都从标准输入读取
            // 1.3.1 输入重定向,可以不做
            dup2(pipefd[0], 0);
            // 1.3.2 子进程开始等待获取命令
            WaitCommand();
            close(pipefd[0]);
            exit(0);
        }

        // 一定是父进程
        //  1.3 关闭不要的fd
        close(pipefd[0]);

        // 1.4 将新的子进程和他的管道写端,构建对象
        end_points->push_back(EndPoint(id, pipefd[1]));

        fds.push_back(pipefd[1]);
    }
}


int ShowBoard()
{
    std::cout << "##########################################" << std::endl;
    std::cout << "|   0. 执行日志任务   1. 执行数据库任务    |" << std::endl;
    std::cout << "|   2. 执行请求任务   3. 退出             |" << std::endl;
    std::cout << "##########################################" << std::endl;
    std::cout << "请选择# ";
    int command = 0;
    std::cin >> command;
    return command;
}

void ctrlProcess(const vector<EndPoint> &end_points)
{
    // 2.1 我们可以写成自动化的,也可以搞成交互式的
    int num = 0;
    int cnt = 0;
    while(true)
    {
        //1. 选择任务
        int command = ShowBoard();
        if(command == 3) break;
        if(command < 0 || command > 2) continue;
        
        //2. 选择进程
        int index = cnt++;
        cnt %= end_points.size();
        std::string name = end_points[index].name();
        std::cout << "选择了进程: " <<  name << " | 处理任务: " << command << std::endl;

        //3. 下发任务
        write(end_points[index]._write_fd, &command, sizeof(command));

        sleep(1);
    }
}

void waitProcess(const vector<EndPoint> &end_points)
{
    // 1. 我们需要让子进程全部退出 --- 只需要让父进程关闭所有的write fd就可以了!
    // for(const auto &ep : end_points) 
    // for(int end = end_points.size() - 1; end >= 0; end--)
    for(int end = 0; end < end_points.size(); end++)
    {
        std::cout << "父进程让子进程退出:" << end_points[end]._child_id << std::endl;
        close(end_points[end]._write_fd);

        waitpid(end_points[end]._child_id, nullptr, 0);
        std::cout << "父进程回收了子进程:" << end_points[end]._child_id << std::endl;
    } 
    sleep(10);

    // 2. 父进程要回收子进程的僵尸状态
    // for(const auto &ep : end_points) waitpid(ep._child_id, nullptr, 0);
    // std::cout << "父进程回收了所有的子进程" << std::endl;
    // sleep(10);
}



int main()
{
    vector<EndPoint> end_points;
    // 1. 先进行构建控制结构, 父进程写入,子进程读取 , bug?
    createProcesses(&end_points);

    // 2. 我们的得到了什么?end_points
    ctrlProcess(end_points);

    // 3. 处理所有的退出问题
    waitProcess(end_points);
    return 0;
}

Task.hpp:

#pragma once

#include <iostream>
#include <vector>
#include <unistd.h>
#include <unordered_map>

// typedef std::function<void ()> func_t;

typedef void (*fun_t)(); //函数指针

void PrintLog()
{
    std::cout << "pid: "<< getpid() << ", 打印日志任务,正在被执行..." << std::endl;
}

void InsertMySQL()
{
    std::cout << "执行数据库任务,正在被执行..." << std::endl;
}

void NetRequest()
{
    std::cout << "执行网络请求任务,正在被执行..." << std::endl;
}


//约定,每一个command都必须是4字节
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQEUST 2

class Task
{
public:
    Task()
    {
        funcs.push_back(PrintLog);
        funcs.push_back(InsertMySQL);
        funcs.push_back(NetRequest);
    }
    void Execute(int command)
    {
        if(command >= 0 && command < funcs.size()) funcs[command]();
    }
    ~Task()
    {}
public:
    std::vector<fun_t> funcs;
};

不知道大家注意到了没有一个问题:我们在创建子进程时先做的工作是先从vector中读取数据来关闭的,这个vector中存放的究竟是什么呢?

 我们来思考下:当我们父进程第一次fork后,父进程使用了下标为4的文件描述符,第一个子进程使用了下标为3的文件描述符,父进程通过pipefd[1]向第一个管道里面写入数据,子进程通过pipefd[0]在管道里面读取数据,但是当我们第二次创建子进程的时候子进程会继承父进程的文件描述符,也就是说,第二次的子进程居然也继承了父进程第一次打开的下标为4的文件描述符,那么这样做的危害是什么?如果我们通过先的关闭第一个管道的写端,然后再回收第一个子进程时,第一个子进程会一直阻塞在那里,为什么呢?因为第二个子进程中继承父进程的写端还指向第一个子进程的读端,也就是我们如果只先关闭了第一个管道的写端是不行的,第一个子进程并没有结束,因为他的读端还指向后面所有的子进程,这就导致第一个子进程回收时一直阻塞在那里。后面进程的分析方法同理:

 解决方法有:我们可以先统一将所有管道的写端关闭,然后再一个一个回收。这样所有进程的写端都被关闭了,自然就成功退出了。还可以从最后一个管道的写端开始关闭,边关闭边回收,由于最后一个子进程的读端只指向最后一个管道的写端,所以能够正常退出。

但是这样写终归治标不治本,这时因为fork创建子进程时子进程已经把前面进程的文件描述符给继承下来了,有没有方法在创建子进程是就把继承父进程的文件描述符给关闭了呢?答案是有的,上面我们提到的vector就是能够很好的处理,我们每次创建了子进程后,就把对应的管道的写端给保留下来(保留到vector中),然后每次创建时就先关闭之前继承的文件描述符给关闭就行了。(这里面的关系有点复杂,大家一定要自己下去好好总结)

所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了 “Linux 一切皆文件思想

2.3 管道读写规则

当没有数据可读时:

  • O_NONBLOCK disableread调用阻塞,即进程暂停执行,一直等到有数据来到为止。
  • O_NONBLOCK enableread调用返回-1errno值为EAGAIN
当管道满的时候:
  • O_NONBLOCK disable write调用阻塞,直到有进程读走数据
  • O_NONBLOCK enable:调用返回-1errno值为EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

 前面的都很好理解,这里提一嘴什么是原子性:原子性就是假如我们往管道中写入一句"hello world"时,如果能够将其完整写进去时进行写入,否则就不进行写入,也就是程序的执行只能够有两种结果:数据全部写入和数据全部都没写入,不存在着写入一半的情况。

2.4 匿名管道特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

 

2.5 命名管道

2.5.1 概念

管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用 FIFO 文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件。

2.5.2 使用

我们知道匿名管道是具有血缘关系的进程建立通信的方式,而命名管道则是可以让没有血缘关系的进程建立通信。那么如何让没有血缘关系的进程看到同一份资源呢?

我们首先来看看这样一个命令:

 我们通过mkfifo命令创建了一个叫做fifo的管道文件,当我们将字符串输出重定向到该文件时我们发现光标卡在了这里不动了,而当我们去读取的时候才会显现,当我们终止掉时两边都已经结束了:

 这是在命令行上创建的命名管道。而这种管道是内存级别的文件,是不会刷新到磁盘上的。生成的命名管道文件只是内核缓冲区的一个标识,用于让多个进程找到同一个缓冲区。匿名管道和命名管道的本质都是内核中的一块缓冲区。再来回答如何让不同的进程看到同一份资源,我们可以采用文件路径+文件名来作为唯一标识该文件的方法来创建文件作为一个命名管道。

除了用命令行式的方法,我们还可以用系统调用:

int mkfifo(const char *filename,mode_t mode);

 通过这个我们可以实现一个简单的服务端与客户端进行通信的程序:

server.cc:

#include<iostream>
#include<cstring>
#include<string>
#include<cerrno>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
using namespace std;


int main()
{
    const string fileName("myfile");
    umask(0);
    int n=mkfifo(fileName.c_str(),0666);
    if(n<0)
    {
        cerr<<"strerrno:"<<errno<<strerror(errno)<<endl;
        return 1;
    }

    cout<<"server creat fifo success"<<endl;
    int rop=open(fileName.c_str(), O_RDONLY);
    if(rop<0)
    {
        cerr<<"strerrno:"<<errno<<strerror(errno)<<endl;
        return 1;
    }
    cout<<"server open fifo success,begin ipc"<<endl;
    char buffer[1024];
    while(true)
    {
        buffer[0]=0;
        int n=read(rop,buffer,sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n]=0;
            cout<<buffer<<endl;
        }
        else if(n==0)
        {
            cout<<"client exit,server also exit"<<endl;
            break;
        }
        else
        {
            cerr<<"strerrno:"<<errno<<strerror(errno)<<endl;
            return 1;
        }
    }
    close(rop);
    unlink(fileName.c_str());
    return 0;
}

client.cc:

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string>
#include<cerrno>
#include<cstring>
using namespace std;
int main()
{
    const string fileName("myfile");
    int wop=open(fileName.c_str(),O_WRONLY);
    if(wop<0)
    {
        cerr<<"strerrno:"<<errno<<strerror(errno)<<endl;
        return 1;
    }
    string myinfo;
    while(true)
    {
        cout<<"请输入你的消息"<<endl;
        getline(cin,myinfo);
        write(wop,myinfo.c_str(),myinfo.size());
        myinfo[strlen(myinfo.c_str())-1]=0;
    }
    close(wop);
    return 0;
}

Makefile:

.PHONY:all
all:client server

client:client.cc
	g++ -o $@ $^ -std=c++11
server:server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -rf client server

效果:

由于我们是在服务端建立好的匿名管道,所以当我们退出时最好在服务端中干掉管道文件。

顺便提问一下:多个进程在通过管道通信时,删除管道文件则无法继续通信吗?

显然不是的,由于管道文件只是起一个标识作用,之前已经打开管道的进程依旧可以正常通信。

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

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

相关文章

Learning C++ No.28 【C++11语法实战】

引言&#xff1a; 北京时间&#xff1a;2023/6/5/9:25&#xff0c;今天8点45分起床&#xff0c;一种怎么都睡不够的感觉&#xff0c;特别是周末&#xff0c;但是如果按照我以前的睡觉时间来看&#xff0c;妥妥的是多睡了好久好久&#xff0c;并且昨天也睡了一天&#xff0c;哈…

C#,码海拾贝(32)——计算“实对称三对角阵的全部特征值与特征向量的”之C#源代码

using System; namespace Zhou.CSharp.Algorithm { /// <summary> /// 矩阵类 /// 作者&#xff1a;周长发 /// 改进&#xff1a;深度混淆 /// https://blog.csdn.net/beijinghorn /// </summary> public partial class Matrix {…

第⑩讲:Ceph集群CephFS文件存储核心概念及部署使用

文章目录 1.CephFS文件存储核心概念1.1.CephFS文件存储简介1.2.CephFS文件存储架构1.3.CephFS文件系统的应用场景与特性 2.在Ceph集群中部署MDS组件3.在Ceph集群中创建一个CephFS文件存储系统3.1.为CephFS文件存储系统创建Pool资源池3.2.创建CephFS文件系统3.3.再次观察Ceph集群…

chatgpt赋能python:从后到前查找Python字符串

从后到前查找Python字符串 Python是一种流行的编程语言&#xff0c;广泛用于Web开发、数据科学和算法设计等领域。其中&#xff0c;字符串是Python编程中的重要概念之一&#xff0c;它不仅可以表示文本&#xff0c;还可以进行各种处理。本篇文章将介绍Python字符串从后到前的查…

chatgpt赋能python:Python如何运行最方便

Python 如何运行最方便 Python 是一种高级编程语言&#xff0c;被广泛使用于各类领域。由于其简单易学&#xff0c;可读性高&#xff0c;适用于不同平台的特性&#xff0c;Python 已成为计算领域、Web 开发、数据分析等领域的首选语言之一。如果您正在学习 Python 或需要对其进…

【SQL】Oracle数据库安装并实现远程访问

文章目录 前言1. 数据库搭建2. 内网穿透2.1 安装cpolar内网穿透2.2 创建隧道映射 3. 公网远程访问4. 配置固定TCP端口地址4.1 保留一个固定的公网TCP端口地址4.2 配置固定公网TCP端口地址4.3 测试使用固定TCP端口地址远程Oracle 前言 Oracle&#xff0c;是甲骨文公司的一款关系…

RTL8380MI/RTL8382MI管理型交换机系统软件操作指南六:RSTP/快速生成树协议

对RSTP/快速生成树协议进行详细的描述&#xff0c;主要包括以下内容&#xff1a;STP概述、RSTP介绍、全局配置、端口配置、RSTP信息、端口信息. 1.1 STP概述 STP&#xff08;Spanning Tree Protocol&#xff09;是生成树协议的英文缩写。STP协议中定义了根桥&#xff08;RootB…

报表生成器FastReport .Net用户指南:显示数据列、HTML标签

FastReport .Net是一款全功能的Windows Forms、ASP.NET和MVC报表分析解决方案&#xff0c;使用FastReport .NET可以创建独立于应用程序的.NET报表&#xff0c;同时FastReport .Net支持中文、英语等14种语言&#xff0c;可以让你的产品保证真正的国际性。 FastReport.NET官方版…

『 前端三剑客 』:HTML常用标签

HTML中常用标签 HTML中常用标签一 . 认识HTML标签二 . HTML标签介绍三 . 案例应用 一 . 认识HTML标签 在HTML中标签是以成对的结构出现的,在HTML当中代码是通过标签来组织的 , 下面通过见得的Hello World的展现来显示歘HTML 标签的结构 <html><head></head>…

mac使用anaconda安装人声分离开源工具spleeter

0. 以下为一步步自己摸索的成功安装过程 1. 安装 spleeter 注&#xff1a;anaconda 的虚拟环境 conda install spleeter太慢 pip install spleeter下载卡住 (tensorflow) Robin-macbook-pro:~ robin$ pip install spleeter Collecting spleeterWARNING: Retrying (Retry(t…

伺服电机的刚性和惯量如何理解

要说刚性&#xff0c;先说刚度。 刚度是指材料或结构在受力时抵抗弹性变形的能力&#xff0c;是材料或结构弹性变形难易程度的表征。 材料的刚度通常用弹性模量E来衡量。在宏观弹性范围内&#xff0c;刚度是零件荷载与位移成正比的比例系数&#xff0c;即引起单位位移所需的力…

OpenAI 领导层建议成立人工智能国际监管组织

人工智能的发展非常迅速&#xff0c;其潜在风险也变得越来越明显&#xff0c;为此&#xff0c;OpenAI的领导层认为&#xff0c;世界需要一个类似于核能监管机构的国际人工智能监管机构--要尽快建立&#xff0c;但也不能操之过急。 在该公司的一篇博文中(https://openai.com/blo…

javaScript蓝桥杯----图⽚⽔印⽣成

目录 一、介绍二、准备三、⽬标四、代码五、完成 一、介绍 很多⽹站都会通过给图⽚添加⽔印的形式来标记图⽚来源&#xff0c;维护版权。前端⽣成⽔印通常是通过canvas 实现&#xff0c;但实际上我们也可以直接利⽤ CSS 来实现图⽚⽔印&#xff0c;这样做会有更好的浏览器兼容…

全球前十!小米积极推动5G标准制定,科技引领高速发展

5G是推动人类社会数字化转型升级的关键支撑&#xff0c;为打造全移动和全连接的智能社会提供技术基础&#xff0c;巨大且深刻地改变着我们的生活。 近日&#xff0c;中国信息通信研究院发布了《全球5G标准必要专利及标准提案研究报告&#xff08;2023年&#xff09;》&#xff…

【新版】系统架构设计师 - 纲要章节汇总

个人总结&#xff0c;仅供参考&#xff0c;欢迎加好友一起讨论 随时更新&#xff0c;请持续关注 … \color{#FF7D00}随时更新&#xff0c;请持续关注… 随时更新&#xff0c;请持续关注… 文章目录 上午题 - 综合知识计算机公共基础知识架构核心知识新增技术知识知识点地图 下午…

免费的配音软件--- tts-vue 软件 下载安装过程

视频效果 tts-vue 软件 图片效果 软件包含有; 语言: 高棉语(柬埔寨) 马耳他语(马耳他) 马来语(马来西亚) 马拉雅拉姆语(印度) 马拉地语(印度) 马其顿语(北马其顿) 韩语(韩国) 阿拉伯语(黎巴嫩) 阿拉伯语(阿昙) 阿拉伯语(阿拉伯联合酋长国) 阿拉伯语(阿尔及利亚) 阿拉伯语(约…

高速信号处理卡 光纤接入卡 设计方案: 519-基于ZU19EG的4路100G光纤的PCIe 加速计算卡

519-基于ZU19EG的4路100G光纤的PCIe 加速计算卡 一、板卡概述 本板卡系我司自主设计研发&#xff0c;基于Xilinx公司Zynq UltraScale MPSOC系列SOC XCZU19EG-FFVC1760架构&#xff0c;支持PCIE Gen3x16模式。其中&#xff0c;ARM端搭载一组64-bit DDR4&#xff0c;总容…

Vue.js 中的 $emit 和 $on 方法有什么区别?

Vue.js 中的 $emit 和 $on 方法有什么区别&#xff1f; 在 Vue.js 中&#xff0c;$emit 和 $on 方法是两个常用的方法&#xff0c;用于实现组件间的通信。它们可以让我们在一个组件中触发一个自定义事件&#xff0c;并在另一个组件中监听这个事件&#xff0c;从而实现组件间的…

【AI实战】大语言模型(LLM)有多强?还需要做传统NLP任务吗(分词、词性标注、NER、情感分类、知识图谱、多伦对话管理等)

【AI实战】大语言模型&#xff08;LLM&#xff09;有多强&#xff1f;还需要做传统NLP任务吗&#xff08;分词、词性标注、NER、情感分类、多伦对话管理等&#xff09; 大语言模型大语言模型有多强&#xff1f;分词词性标注NER情感分类多伦对话管理知识图谱 总结 大语言模型 大…

PIC16F18877学习(一)

为什么要在PIC控制器中使用#pragma configs 这些设置位于程序代码之外的闪存中。 PIC一通电&#xff0c;它们就可用了&#xff0c;无论它们写在代码的哪个位置。这很重要&#xff0c;因为有时在执行程序之前需要它们。例如&#xff0c;有一些设置可以选择时钟源&#xff0c;并…