【Linux】进程间通信之管道

news2024/11/23 3:59:58

目录

  • 🌈前言
  • 🌸1、IPC介绍
    • 🍢1.1、进程间通信的目的
    • 🍡1.2、背景和发展
    • 🍠1.3、进程间通信的分类
  • 🌷2、管道
    • 🍡2.1、概念
    • 🍢2.2、管道的原理
    • 🍣2.3、匿名管道
    • 🍤2.4、管道的读写规则
    • 🍥2.5、多进程间通信
    • 🍦2.6、管道的特点
  • 🌸3、命名管道
    • 🍧3.1、概念
    • 🍨3.2、创建命名管道
    • 🍩3.3、命名管道的使用
    • 🍪3.4、匿名管道与命名管道的区别
    • 🍫3.5、命名管道的读写规则

🌈前言

这篇文章给大家带来进程间通信的学习!!!


🌸1、IPC介绍

🍢1.1、进程间通信的目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程

  • 资源共享:多个进程之间共享同样的资源!!!

  • 通知事件:一个进程需要向一个或一组进程发送消息,通知它们发生了某种事情(如:子进程终止时,需要通知父进程)

  • 进程控制:有些时候进程希望完全控制另一个进程的执行(如:gdb进程控制另一个进程进行调试),此时控制进程希望能够拦截另一个进程的所有陷入的异常,并能够及时知道它的状态改变


🍡1.2、背景和发展

  • 我们都知道:进程间居有独立性,互不干扰。进程间想要通信(交互数据:需要多个进程协同处理一件事情!!!),成本会很高

  • 进程虽然具有独立性(写时拷贝),但不是彻底独立了,有时候,我们需要二个进程进行信息交互,这时候我们就需要打破它们的独立性,进行数据交互

进程间通信的发展:

  • 管道:它是Linux系统一下一种IPC通信机制,可以用于进程间通信,线程间通信

  • System V进程间通信:用于在操作系统层面上进行进程间通信的标准,它给用户提供了系统接口,调用它的接口就能完成进程间的通信

  • POSIX进程间通信:它是System V 进程间通信的变体


🍠1.3、进程间通信的分类

管道:

  • 匿名管道

  • 命名管道

System V进程间通信:

  • System V消息队列

  • System V共享内存

  • System V信号量

POSIX进程间通信:

  • 消息队列

  • 共享内存

  • 信号量

  • 互斥量

  • 条件变量

  • 读写锁


🌷2、管道

🍡2.1、概念

  • 管道分为:匿名管道命名管道,管道是提供共享资源的一种手段!!!

  • 管道是Unix操作系统中最古老的进程间通信的形式

  • 我们把从一个进程连接到另一个进程的一个“数据流”称为一个“管道”

该图中:who进程以写的方式打开管道,向管道写入数据,wc进程以读的方式打开,读取who进程写入的数据,对这些数据进行处理!!!

在这里插入图片描述

  • 我们学习IPC,是让不同的进程看到同一份资源(文件,内存块,队列,网络等等)

  • 学习IPC,不是先学习如何通信,而是先让多个进程如何看到同一份资源!!!

  • 资源的不同,就决定了不同的通信方式(资源是队列时,就是以消息队列方式通信)


🍢2.2、管道的原理

管道原理:

  • struct file结构体里面有一个缓冲区(struct address_space)

  • 一个文件想知道自己是什么文件,会找到struct inode中的union联合体里面的指针,如果是块设备,就会生效块设备的指针,该指针指向描述对应设备的结构体!!!

  • 知道自己是什么文件的时候,就可以打开对应的缓冲区!!!

pipe_inode_info是管道、block_device是块设备、cdev是字符设备

在这里插入图片描述

  • 对应设备的结构体里面就会包含对应的缓冲区,如下图:

下面是管道描述的结构体,有等待队列(wait)和管道缓冲区(bufs)等等…

在这里插入图片描述


管道原理:
在这里插入图片描述

当子进程执行完毕时,父进程是如何发现的呢???

  • 子进程退出,记录文件描述符指针的计数器会自减,这样父进程就知道子进程执行完了

  • 结论:通过引用计数的方式知道子进程退出了!!!


🍣2.3、匿名管道

匿名管道:没有命名的管道,使用pipe系统调用来创建

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

使用匿名管道,实现一个简单的进程间通信:父进程写,子进程读取数据!!!

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

int main()
{
    // 创建管道 -- 打开二个文件 -- 分别以读、写的方式打开
    int pipefd[2] = {0};
    int flag = pipe(pipefd);
    if (flag == -1)
    {
        cerr << "pipe error" << endl;
        exit(1);
    }
    pid_t id = fork(); // 创建子进程

    // 父子进程分流执行各自代码
    if (id == 0)
    {
        // Chile Process -- 子进程进行读操作,需要关闭写端文件
        close(pipefd[1]);
        char buffer[64];
        while (true)
        {
            memset(buffer, 0, sizeof(buffer)); // 每次重新初始化buffer
            ssize_t s = read(pipefd[0], buffer, sizeof(buffer));
            if (s > 0)
            {
                cout << time_t(time(nullptr)) << endl;
                cout << "子进程收到消息,内容是:" << buffer << endl;
            }
            else if (s == 0)
            {
                cout << "父进程写完了,我也退出了!!!" << endl;
                break;
            }
            else if (s == -1)
            {
                cerr << "read erron" << endl;
                exit(3);
            }
        }
        close(pipefd[0]);
        exit(0);
    }
    else if (id > 0)
    {
        // Parent Process -- 父进程写入数据,需要关闭读端文件
        close(pipefd[0]);
        const char *msg = "hello world!!!";
        int cnt = 0;
        while (cnt < 5)
        {
            write(pipefd[1], msg, strlen(msg));
            ++cnt;
            sleep(2);
        }

        close(pipefd[1]);
        cout << "父进程写完了!!!" << endl;
    }
    else
    {
        // Do Nothing
    }
    int status = 0;
    pid_t ret = waitpid(-1, &status, 0); // 阻塞等待
    if (ret > 0)
    {
        cout << "等待子进程成功,退出码:" << WEXITSTATUS(status) << ", 退出信号:" << WTERMSIG(status) << endl;
    }
    // pipefd[0] --> 读
    // pipefd[1] --> 写
    // cout << "pipefd[0]: " << pipefd[0] << endl
    //      << "pipefd[1]: " << pipefd[1] << endl;
    return 0;
}

在这里插入图片描述

用fork来共享管道原理

  • 原理:创建管道后,父进程创建子进程,子进程继承了父进程的代码和数据

  • 父子进程都以读写的方式指向管道的两端

  • 如果父进程要读,子进程要写,那么父进程必须关闭写端,子进程关闭读端

在这里插入图片描述

站在文件描述符角度-深度理解管道

在这里插入图片描述

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

  • 管道被创建出来后,在磁盘中只是一个标识符,主要是在内存中进行通信(大小一般为:4KB)

为什么父进程要分别以读和写的方式创建管道文件?

  • 因为为了让子进程继承父进程,让子进程不用重新打开了!

为什么父子进程要关闭对应的读写?

  • 因为管道是单向的!

  • 只能一边流向一边,不能二端一起互相流动,跟管子一样!


🍤2.4、管道的读写规则

当管道没有数据可读时:

  • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止

  • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN

当管道满的时候:

  • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据

  • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

测试:父进程一直写入数据,子进程一直不读,最后父进程write会不会阻塞?

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

int main()
{
    // 创建管道 -- 打开二个文件 -- 分别以读、写的方式打开
    int pipefd[2] = {0};
    int flag = pipe(pipefd);
    if (flag == -1)
    {
        cerr << "pipe error" << endl;
        exit(1);
    }
    pid_t id = fork(); // 创建子进程

    // 父子进程分流执行各自代码
    if (id == 0)
    {
        // Chile Process -- 子进程进行读操作,需要关闭写端文件
        close(pipefd[1]);
        char buffer[64];
        
        while (true)
        {}
        
        close(pipefd[0]);
        exit(0);
    }
    else if (id > 0)
    {
        // Parent Process -- 父进程写入数据,需要关闭读端文件
        close(pipefd[0]);
        const char *msg = "hello world!!!";
        int cnt = 0;
        while (1)
        {
            write(pipefd[1], msg, strlen(msg));
            ++cnt;
            cout << "父进程写入的次数:" << cnt << endl;
        }

        close(pipefd[1]);
        cout << "父进程写完了!!!" << endl;
    }
    else
    {
        // Do Nothing
    }
    int status = 0;
    pid_t ret = waitpid(-1, &status, 0); // 阻塞等待
    if (ret > 0)
    {
        cout << "等待子进程成功,退出码:" << WEXITSTATUS(status) << ", 退出信号:" << WTERMSIG(status) << endl;
    }
    return 0;
}

在这里插入图片描述

  • 如果所有管道写端对应的文件描述符被关闭,则read返回0

  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出

原子性:在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体

  • 当要写入的数据量不大于PIPE_BUF(管道缓冲区最大值)时,Linux将保证写入的原子性

  • 当要写入的数据量大于PIPE_BUF(管道缓冲区最大值)时,Linux将不再保证写入的原子性

man 7 pipe手册中的PIPE_BUF介绍

在这里插入图片描述

总结:

  • 管道内部实现自带同步和互斥的机制! – 按顺序进行写数据和读数据

  • 正常的多进程在对硬件(比如:显示器)进行写入时,打印的顺序是乱的


🍥2.5、多进程间通信

多进程间通信(进程池):

  • 创建一个父进程、多个管道和多个子进程(管道与子进程数量一致)

  • 父进程向不同的子进程发送任务码,子进程收到任务码后会执行对应的任务!

在这里插入图片描述

#include <iostream>
#include <vector>
#include <unordered_map>
#include <ctime>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

typedef void (*method)(); // 定义一个函数指针类型
vector<method> methods;   // 方法集合(任务集合)

unordered_map<uint32_t, string> info;

// 用来保存父进程的PID和对应的读端文件描述符(pipefd[0])
vector<pair<uint32_t, uint32_t>> v;

void Func1()
{
    cout << "这是一个处理日志的任务, 执行进程的ID是: " << getpid()
         << ",执行时间是: " << time(nullptr) << endl;
}

void Func2()
{
    cout << "这是一个备份数据的任务, 执行进程的ID是: " << getpid()
         << ",执行时间是: " << time(nullptr) << endl;
}

void Func3()
{
    cout << "这是一个处理网络服务的任务, 执行进程的ID是: " << getpid()
         << ",执行时间是: " << time(nullptr) << endl;
}

// 初始化任务列表
void Methods_Init()
{
    info.insert(make_pair(methods.size(), "处理日志"));
    methods.push_back(Func1);
    info.insert(make_pair(methods.size(), "备份数据"));
    methods.push_back(Func2);
    info.insert(make_pair(methods.size(), "处理网络服务"));
    methods.push_back(Func3);
}

// 父进程写入数据
void Work(int fd)
{
    while (true)
    {
        uint32_t operatorType = 0;
        ssize_t s = read(fd, &operatorType, sizeof(uint32_t));
        if (s == 0)
        {
            cout << "父子进程的写端已经关闭, 那我子进程也退出了! ! !" << endl;
            break;
        }
        if (s == sizeof(uint32_t))
        {
            if (operatorType < methods.size())
            {
                methods[operatorType](); // 调用对应的方法!
            }
        }
    }
}

// 子进程读取数据
void DispatchTask(vector<pair<uint32_t, uint32_t>> &vv)
{
    srand((unsigned int)time(nullptr)); // 随机数种子
    int cnt = 3;
    while (cnt--)
    {
        // 选择一个进程
        uint32_t pick = rand() % vv.size();

        // 选择一个任务
        uint32_t task = rand() % methods.size();

        // 写入指定进程的管道中
        write(vv[pick].second, &task, sizeof(uint32_t));
        cout << "父进程指派任务, 该任务是: " << info[task]
             << ", 时间是: " << time(nullptr) << ", 编号是: " << pick << endl;
        sleep(1);
    }
}

// 多进程间通信 -- 父进程对一组子进程派发任务
int main()
{
    // 0、加载任务列表
    Methods_Init();
    // 2、创建多进程 并且 各自创建一个父子进程通信的管道(每个子进程配对一个管道)
    int Process_Num = 3;
    for (int i = 0; i < Process_Num; ++i)
    {
        // 1、创建管道
        int pipefd[2] = {0};
        if (pipe(pipefd) == -1)
        {
            cerr << "pipe erron: " << endl;
            return 1;
        }
        pid_t id = fork();
        if (id == 0)
        {
            // 3、Child Process -- 子进程读数据,关闭写端
            close(pipefd[1]);
            // 子进程执行父进程派发的任务
            Work(pipefd[0]);
            exit(0);
        }
        // if以外的都是父进程的操作!
        // 4、父进程写数据,关闭读端
        close(pipefd[0]);
        // 将父进程的PID和读端文件描述符写入到v中
        v.push_back(make_pair(id, pipefd[0]));
    }
    // 父进程派发任务给子进程
    DispatchTask(v);

    // 5、阻塞等待子进程退出
    for (int i = 0; i < Process_Num; ++i)
    {
        close(v[i].second); // 逐个关闭父进程写端
        int status = 0;
        pid_t ret = waitpid(v[i].first, &status, 0); // 阻塞等待
        if (ret > 0)
        {
            cout << "父进程等待子进程[" << v[i].first << "]成功, 退出码: "
                 << WEXITSTATUS(status) << ", 退出信号: " << WTERMSIG(status) << endl;
        }
    }
    return 0;
}

🍦2.6、管道的特点

  • 只能用于具有共同祖先的进程(具有血缘关系的进程)之间进行通信。通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道

  • 管道提供流式服务 – 单向通信(一个进程的数据流入到另一个进程进行处理)

  • 一般而言,进程退出,管道释放(管道也是文件),所以管道的生命周期随进程

  • 一般而言,内核会对管道操作进行同步与互斥(管道读写规则)

  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

  • 管道是面向字节流进行传输数据的!!!

在这里插入图片描述


🌸3、命名管道

🍧3.1、概念

  • 使用管道通信的限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信

  • 如果我们想在不相关的进程(不是fork出来的进程)之间交换数据,可以使用FIFO文件(管道文件)来做进行通信,它经常被称为:命名管道

  • 命名管道是一种特殊类型的文件,文件类型为:p


🍨3.2、创建命名管道

使用指令创建命名管道:

命名管道可以从命令行上创建,命令行方法是使用下面这个命令:mkfifo filename filename

[lyh@localhost Test]$ ll
总用量 0
[lyh@localhost Test]$ mkfifo FIFO
[lyh@localhost Test]$ ll
总用量 0
prw-rw-r--. 1 lyh lyh 0 1219 01:51 FIFO

在这里插入图片描述

命名管道也可以从程序里创建,相关函数有:

int mkfifo(const char *pathname,mode_t mode);
  • 返回值:成功时,mkfifo()返回0,如果出现错误,则返回-1(在这种情况下,错误号设置为:appropri‐ately)

  • pathname:在哪个路径下创建什么名字的管道文件

  • mode:该参数指定FIFO文件的权限(八进制设置),该参数会受到已继承父进程中umask的影响,因此:创建的文件的权限是(mode & ~umask)

简单创建一个命名管道:

int main()
{
    // 设置当前进程权限掩码
    umask(0);
    // 在进程当前工作路径(cwd)创建一个FIFO管道文件,文件权限为664
    if (mkfifo("./FIFO", 0664) != 0)
    {
        cerr << "mkdfifo error: " << endl;
        return 1;
    }
    return 0;
}

🍩3.3、命名管道的使用

使用命名管道完成没有血缘关系的进程之间的通信:

  • 服务器写入数据,客户端读取数据,并且向显示器进程刷新

  • serverFifo是服务器文件,clientFifo是客户端文件

  • 主要知识用到了:mkfifo、open、read、write等接口

comm.h

#pragma once

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

// 命名管道创建的路径及管道的命名
#define PIPE_PATH "./pipe"

// 写入数据的最大数量
#define NUM 1024

serverFifo.cpp

#include "comm.h"

// 服务器读取客户端发送的数据
int main()
{
    // 设置当前进程权限掩码
    umask(0);
    // 1、创建命名管道
    if (mkfifo(PIPE_PATH, 0664) != 0)
    {
        cerr << "mkdfifo error: " << endl;
        return 1;
    }

    // 打开文件 -- 只写状态打开文件
    int pipefd = open(PIPE_PATH, O_RDONLY);
    if (pipefd < 0)
    {
        cerr << "open error: " << endl;
        return 2;
    }
    // 向命名管道读取数据
    char buffer[NUM];
    while (true)
    {
        ssize_t R = read(pipefd, buffer, sizeof(buffer));
        if (R > 0)
        {
            // 清除回车
            buffer[R] = '\0';
            cout << "客户端->服务器: " << buffer << endl;
        }
        else if (R ==0)
        {
            cout << "客户端退出了,我也退出了!!!" << endl;
            break;
        }
        else
        {
            cerr << "read error: " << endl;
            return 3;
        }
    }
    close(pipefd);
    // 删除管道文件
    unlink(PIPE_PATH);
    cout << "服务器退出!" << endl;
    return 0;
}

clientFifo.cpp

#include "comm.h"

// 客户端向命名管道写入数据
int main()
{

    // 打开命名管道文件,以只写的方式打开
    int pipefd = open(PIPE_PATH, O_WRONLY);
    if (pipefd < 0)
    {
        cerr << "open error: " << endl;
        return 2;
    }
    // 向命名管道写入数据
    char buffer[64];
    while (true)
    {
        memset(buffer, 0, sizeof(buffer));
        if (fgets(buffer, sizeof(buffer), stdin) != nullptr)
        {
            printf("请输入内容:");
            fflush(stdout); // 刷新输出缓冲区
            ssize_t W = write(pipefd, buffer, strlen(buffer));
            if (W > 0)
            {
                buffer[W] = '\0';
            }
        }
        else
        {
            break;
        }
    }
    close(pipefd);
    cout << "客户端退出了!!!" << endl;
    return 0;
}

🍪3.4、匿名管道与命名管道的区别

  • 匿名管道由pipe函数创建并打开

  • 命名管道由mkfifo函数创建,打开用open

  • FIFO(命名管道)pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义


🍫3.5、命名管道的读写规则

如果当前打开操作是为读而打开FIFO时:

  • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO

  • O_NONBLOCK enable:立刻返回成功

如果当前打开操作是为写而打开FIFO时

  • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO

  • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

总结:

  • 命名管道和匿名管道只有创建与打开的方式不同之外,读写规则都是一样的

  • 命名管道和匿名管道都会互相同步和互斥!!!

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

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

相关文章

DOM算法系列004-判断给定节点是否为body元素

UID: 20221218221939 aliases: tags: source: cssclass: created: 2022-12-18 如果我们要判定给定的一个节点是不是页面body与元素节点&#xff0c;该如何判断呢&#xff1f; 一般来说&#xff0c; 一个HTML页面内只有一个body元素 但是&#xff0c;如果我们非要在页面内写超过…

Spring boot 整合 redis

Spring boot 整合 redis一、Spring boot 整合redis1.1 启动redis1.2 redis desktop manager1.3 常用命令二、操作2.1 依赖包2.2 配置2.3 简单测试2.4 StringRedisTemplate一、Spring boot 整合redis 1.1 启动redis 命令行启动 redis-server redis-cli1.2 redis desktop mana…

基于electronbot作品bootLoader设计

文章目录 前言 一、芯片程序区规划和流程 1、flash区规划 2、两区运行流程 3、bootLoader代码体现 4、electronbot代码体现&#xff1a; 二、bootLoader代码设计 1.下载程序步骤 2.通讯协议格式 三、libusb开发及需要注意的事情 1、bootLoader复合设备 2、electronbot复合设备…

基础算法系列--[基本数据结构KMP]

文章目录前言链表单链表双链表栈和队列栈队列单调KMP前言 今天要搞的是基本的一些数据结构&#xff0c;当然咱们这个不是那么“正经”。当然今天也没啥代码&#xff0c;因为太简单了&#xff08;其实我也想水一下~&#xff09; 链表 单链表 单链表这个东西&#xff0c;应该…

Prometheus+Grafana

K8S prometheusK8S1.Prometheus&#xff08;普罗米修斯&#xff09;2.Prometheus可以做什么3.Prometheus的特点4.prometheus 相关组件二、prometheus与zabbix的区别zabbix架构区别三、prometheus架构分析1.TSDB2.时间序列数据库的特点3.prometheus 相关组件1.prometheus 核心组…

【计算机网络】实验五 网络层与链路层协议分析(PacketTracer)

一、实验目的 通过本实验&#xff0c;进一步熟悉PacketTracer的使用&#xff0c;学习路由器与交换机的基本配置&#xff0c;加深对网络层与链路层协议的理解。 二、实验内容&#xff1a; 4.1 路由器交换机的基本配置 打开下面的实验文件&#xff0c;按提示完成实验。 4.2…

直流微电网中潮流(Matlab代码实现)

目录 1 概述 1.1 直流电网中的潮流 1.2 创新点和相关工作 1.3 本文结构 2 数学/网络模型 2.1 主-从操作 2.2 孤岛运行 3 牛顿法 4 案例及Matlab代码实现 1 概述 潮流是一个非线性问题&#xff0c;需要用牛顿法求解具有恒定功率端子的直流微电网。本文提出了牛顿法在…

曙光来临!Nature终于发现了新冠特效药?或将彻底终结新冠时代!

百趣代谢组学文献分享&#xff1a;2022年即将过去&#xff0c;随着疫苗的全面接种和三年以来“动态清零”的坚持&#xff0c;我们在应对新冠病毒如潮水般的攻击中取得了阶段性成果。虽然大家陆陆续续投入到正常的工作生活中&#xff0c;但是我们都知道新冠并未被“打败”&#…

MySQL中这14个有用的小知识,快学起来吧

前言 我最近用MYSQL数据库挺多的&#xff0c;发现了一些非常有用的小玩意&#xff0c;今天拿出来分享到大家&#xff0c;希望对你会有所帮助。 1.group_concat 在我们平常的工作中&#xff0c;使用group by进行分组的场景&#xff0c;是非常多的。 比如想统计出用户表中&…

如何在产品开发中讨论概念设计?

每当你看到一辆在路上行驶的汽车、书桌上的笔记本电脑、工业包装生产线、医院设备、家用仪器和其他形式的概念设计创意产品会感到难以置信&#xff0c;这就是我们在产品开发中讨论概念设计的原因。 概念设计是一个尚未解决或到目前为止尚未令人满意的问题。这是一个深思熟虑的解…

研究区域制图 | 在 ArcGIS Pro中创建地图布局

研究区域制图 | 在 ArcGIS Pro中创建地图布局 数据准备 首先需要两个图层&#xff0c;一个是市区图层&#xff0c;一个是省行政区划图层&#xff0c;我这里以吉林省以及吉林省长春市为例 新建布局 选择横向A5即可 添加参考线 不知道你们知不知道这个功能&#xff0c;反正小…

kotlin协程笔记:Dispatchers

Kotlin 的 launch 会调用 startCoroutineCancellable()&#xff0c;接着又会调用 createCoroutineUnintercepted()&#xff0c;最终会调用编译器帮我们生成 SuspendLambda 实现类当中的 create() 方法。 public fun CoroutineScope.launch(context: CoroutineContext EmptyC…

【JVM】本地方法栈与堆与方法区

文章目录1. 本地方法栈2. 堆3. 方法区1. 本地方法栈 本地方法栈和虚拟机栈有点类似&#xff0c;均具有线程隔离的特点以及都能抛出StackOverflowError和OutOfMemoryError异常。 但是不同之处在于本地方法栈服务的对象是JVM执行的native方法&#xff0c;而虚拟机栈服务的是JVM…

[附源码]Nodejs计算机毕业设计教师职称评定系统Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…

免费提供POSMV的GNSS数据解算服务,验潮仪丢失的一种补救

前两天有个网友问干活的区域附近是否有长期验潮站&#xff0c;因为他的临时验潮仪丢失了&#xff0c;随后问了一下搞水文的同事&#xff0c;他推了一个网址&#xff1a; http://publictide.nmdis.org.cn/tide?SiteGroup3&TideType0&#xff0c;中文名叫&#xff1a;潮汐潮…

C型利钠肽 ,101135-67-5

Bz-VGR-pNA, chromogenic substrate for trypsin and for bacterial trypsin-like proteases.Bz-VGR-pNA&#xff0c;胰蛋白酶和细菌胰蛋白酶样蛋白酶的显色底物。 编号: 127015中文名称: C型利钠肽 (TYR0)-C-PEPTIDE (DOG)英文名: (Tyr0)-C-Peptide (dog)CAS号: 101135-67-5单…

机房动环监控系统3大价值,第一个太惊艳了

在中小学、大学院校中&#xff0c;机房已经是不可缺少的部分&#xff0c;但由于管理缺陷、设备复杂等缘故&#xff0c;学校机房得不到安全保障。 因此&#xff0c;要实现学校机房监控系统&#xff0c;来对机房的运行情况实时监测&#xff0c;以此提高风险预防及设备运行环境质量…

C++利用模板实现消息订阅和分发

解耦是编写程序所遵循的基本原则之一&#xff0c;多态是提高程序灵活性的重要方法。C语言支持重载&#xff0c;模板&#xff0c;虚函数等特性&#xff0c;为编写高性能可扩展的程序提供了利器。编写大型项目时&#xff0c;免不了需要各个模块之间相互调用&#xff0c;从而产生了…

【LeetCode题目详解】(四)20.有效的括号、225.用队列实现栈

目录 一、力扣第20题&#xff1a;有效的括号 1.解题思路 2.写出代码 3.完整代码 二、力扣第225题&#xff1a;用队列实现栈 1.思路分析 2.代码实现 3.完整代码 总结 一、力扣第20题&#xff1a;有效的括号 题目链接&#xff1a;20. 有效的括号 - 力扣&#xff08;Leetc…

计算机毕设Python+Vue学生寝室管理系统(程序+LW+部署)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…