[ Linux ] 线程独立栈,线程分离,Linux线程互斥

news2025/1/23 10:43:13

目录

1.线程栈

1.1pthread_t

1.2用户级的线程id与内核LWP的对应关系

2.分离线程

2.1 pthread_detch

3.线程互斥

3.1互斥相关概念

3.2 互斥量mutex

3.3 售票系统案例验证共享变量会有问题

3.4 解决抢票问题

3.5互斥量的接口

3.5.1初始化互斥量

3.5.2 销毁互斥量

3.5.3 编码

3.5.4 互斥锁的相关问题


1.线程栈

我们使用的线程库是用户级线程库(pthread),我们使用 ldd mythread 可以查看mythread的链接信息。

因此对于一个线程(tast_struct)都是通过在共享空间内执行pthread_create执行线程创建的。所有的代码执行都是在进程的进程地址空间内进行执行的。在了解这些基本的概念之后,我们回顾上一篇的一个问题,pthread_t究竟是什么呢?

1.1pthread_t

上篇文章我就提到过pthread_t是一个无符号长整型整数,但是并没有说pthread_t具体是什么?但是我们把他转化为一个16进制数字时,我们发现这个数字特别想一个地址,那么这里我们需要确认的是,pthread_t 线程Id就是一个地址。而是什么地址呢?我们这里需要知道的是,线程的全部实现,并没有全部体现在操作系统内,而是操作系统提供执行流,具体的线程结构由库来进行管理。库可以创建多个线程->因此库也要管理线程。而库要管理线程也是要先描述再组织。因此在共享区里面包含了struct thread_info,里面就会保存pthread_t tid,线程私有栈等。而申请一个新的线程,库就又会在共享区内创建该线程对应的tid,私有栈等等。而返回的就是该结构的地址。因此pthread_t里面保存的就是对应用户级线程的控制结构体的起始地址!

  • 主线程的独立栈结构,用的就是地址空间内的栈区
  • 新线程用的栈结构,用的是库中提供的栈结构

pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL(原生线程库)实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

Linux中,用户级线程库和LWP是1:1的。

1.2用户级的线程id与内核LWP的对应关系

我们刚刚已经知道了用户级线程id和内核LWP的对应是1:1的。那么我们如果使用代码来验证一下呢?

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>//仅仅是了解

using namespace std;

// 带__thread 给每个线程拷一份
__thread int global_value = 100;

void *startRoutine(void *args)
{
    while (true)
    {
        cout << "thread " << pthread_self() << " global_value: "
             << global_value << " &global_value: " << &global_value
             << " Inc: " << global_value++ << " lwp: " << ::syscall(SYS_gettid) << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;

    pthread_create(&tid1, nullptr, startRoutine, (void *)"thread 1");
    pthread_create(&tid2, nullptr, startRoutine, (void *)"thread 2");
    pthread_create(&tid3, nullptr, startRoutine, (void *)"thread 3");

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);
    pthread_join(tid3, nullptr);

    return 0;
}

我们同样使用监控脚本来看看当前系统下的LWP

while :; do ps -aL |head -1 && ps -aL|grep mythread;sleep 1;done

通过打印的结果我们发现 是能够看到用户级线程id和内核LWP的对应是1:1的。

2.分离线程

  • 默认情况下,新建线程是joinable的,joinable就是可join的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏
  • 如果不关心线程的返回值,join的一种负担;这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

在什么时候下会使用线程分离呢?

我们都知道主线程会join等待新线程,如果新线程一直不退出,主线程就会一直等待,等新线程退出之后释放新线程的资源,这与我们的进程阻塞式等待类似。如果当主线程并不关心或者不需要新线程的退出码时,新线程可以自己退出后自己释放自己的资源。那么主线程就可以不需要等待新线程了。这就完成了线程间的解耦。也叫做线程分离。

2.1 pthread_detch

  • 函数原型:
    • int pthread_detach(pthread_t thread);
  • 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
    • pthread_detach(pthread_self());

注意:joinable和分离是冲突的,一个线程不能即是joinable的又是分离的。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h>//仅仅是了解

using namespace std;

// 带__thread 给每个线程拷一份
__thread int global_value = 100;

void *startRoutine(void *args)
{
    //线程分离
    //pthread_detach(pthread_self());
    while (true)
    {
        cout << "thread " << pthread_self() << " global_value: "
             << global_value << " &global_value: " << &global_value
             << " Inc: " << global_value++ << " lwp: " << ::syscall(SYS_gettid) << endl;
        sleep(1);
    }
}

int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;

    pthread_create(&tid1, nullptr, startRoutine, (void *)"thread 1");
    pthread_create(&tid2, nullptr, startRoutine, (void *)"thread 2");
    pthread_create(&tid3, nullptr, startRoutine, (void *)"thread 3");

    sleep(1);

    //倾向于让主线程分离其他线程
    pthread_detach(tid1);
    pthread_detach(tid2);
    pthread_detach(tid3);

    //一旦分离不能join
    int n1 = pthread_join(tid1, nullptr);
    cout<<"strerror(n1): "<< strerror(n1)<<endl;
    int n2 = pthread_join(tid2, nullptr);
    cout<<"strerror(n2): "<< strerror(n2)<<endl;

    int n3 = pthread_join(tid3, nullptr);
    cout<<"strerror(n3): "<< strerror(n3)<<endl;

    return 0;
}

通过这个实验也验证了一个线程不能即是detach又被join的。

3.线程互斥

3.1互斥相关概念

  • 临界资源:多线程执行流共享的资源就叫做临界资源
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
  • 互斥:任何时刻,互斥保证有且仅有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  • 原子性:不会被恩和调度机制打断的操作,该操作只有两种状态,要么完成,要么未完成。

3.2 互斥量mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,会带来一些问题。

3.3 售票系统案例验证共享变量会有问题

为了验证共享变量会出问题的情况,我们模拟实现一个售票系统的案例。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h> //仅仅是了解

using namespace std;

int tickets = 10000; // 临界资源

void *getTickets(void *args)
{
    const char *name = static_cast<const char *>(args);
    while (true)
    {
        //临界区
        if (tickets > 0)
        {
            cout << name << " 抢到了票,票的编号是:" << tickets << endl;
            tickets--;
        }
        else
        {
            
            cout << name << " 已经放弃抢票了,因为没有票了....." << endl;
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;

    pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");
    pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");
    pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");

    // sleep(1);

    // // 倾向于让主线程分离其他线程
    // pthread_detach(tid1);
    // pthread_detach(tid2);
    // pthread_detach(tid3);

    // 一旦分离不能join
    int n1 = pthread_join(tid1, nullptr);
    cout << "strerror(n1): " << strerror(n1) << endl;
    int n2 = pthread_join(tid2, nullptr);
    cout << "strerror(n2): " << strerror(n2) << endl;
    int n3 = pthread_join(tid3, nullptr);
    cout << "strerror(n3): " << strerror(n3) << endl;

    return 0;
}

执行结果我们可以发现,看似好像没有什么问题,但是其实是存在bug的。在这段代码中

这一段代码是既对票做判断,又对票做--,--是并不是由一条语句执行的,而是被翻译成3条语句执行的。

CPU对tickets--这句话,要翻译成:

  1. 取数据。将数据从内存取到cpu寄存器内
    1. load :将共享变量ticket从内存加载到寄存器中
  1. 做运算。在寄存器内对数据进行运算。
    1. update : 更新寄存器里面的值,执行-1操作
  1. 写回数据。将数据从寄存器写回内存。
    1. store :将新值,从寄存器写回共享变量ticket的内存地址

我们可以看看ticket--部分的汇编代码:

取出ticket--部分的汇编代码

objdump -d a.out > test.objdump

152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>

153 400651: 83 e8 01 sub $0x1,%eax

154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>

而这3个步骤中,线程在任何地方都有可能切换走,而CPU内的寄存器是被所有的执行共享的,但是寄存器里面的数据是属于当前执行流的上下文数据。因此线程要被切换的时候,需要保存上下文;线程要被换回的时候,需要恢复上下文。

因此为了从程序中看到可能错误的数据,我们需要加一个usleep来模拟漫长的业务过程,可能有很多个线程会进入该代码段。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h> //仅仅是了解

using namespace std;

// // 带__thread 给每个线程拷一份
// __thread int global_value = 100;

// void *startRoutine(void *args)
// {
//     //线程分离
//     //pthread_detach(pthread_self());
//     while (true)
//     {
//         cout << "thread " << pthread_self() << " global_value: "
//              << global_value << " &global_value: " << &global_value
//              << " Inc: " << global_value++ << " lwp: " << ::syscall(SYS_gettid) << endl;
//         sleep(1);
//     }
//}

int tickets = 10000; // 临界资源

void *getTickets(void *args)
{
    const char *name = static_cast<const char *>(args);
    while (true)
    {
        //临界区
        if (tickets > 0)
        {
            usleep(1000);//模拟漫长的业务
            cout << name << " 抢到了票,票的编号是:" << tickets << endl;
            tickets--;
        }
        else
        {
            
            cout << name << " 已经放弃抢票了,因为没有票了....." << endl;
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;

    pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");
    pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");
    pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");

    // 一旦分离不能join
    int n1 = pthread_join(tid1, nullptr);
    cout << "strerror(n1): " << strerror(n1) << endl;
    int n2 = pthread_join(tid2, nullptr);
    cout << "strerror(n2): " << strerror(n2) << endl;
    int n3 = pthread_join(tid3, nullptr);
    cout << "strerror(n3): " << strerror(n3) << endl;

    return 0;
}

此时我们确实看到了,产生了脏数据。

3.4 解决抢票问题

要解决以上的问题,我们需要做到三点:

  1. 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  2. 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  3. 如果线程不在临界区中执行,那么该线程不能组织其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫做互斥量。

在临界区内,只能够允许一个线程执行,不允许多个线程同时执行,因此一旦我们给买票的过程加上一把锁,在某一时刻,只能够允许一个线程买票,因此可以保证整个买票的过程是原子的。

3.5互斥量的接口

3.5.1初始化互斥量

申请锁:

初始化互斥量的两种方法:

  • 方法一:静态分配
    • pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER、
  • 方法二:动态分配
    • int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
      • 参数:
        • mutex:要初始化的互斥量
        • attr:NULL

3.5.2 销毁互斥量

释放锁:

销毁互斥量需要注意:

  • 使用PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再次尝试加锁

函数原型:int pthread_mutex_destroy(pthread_mutex_t *mutex);

3.5.3 编码

在我们上述的售票系统中,其中很明显的是,票数tickets属于临界资源,我们需要对其进行加锁。

在我们申请锁成功之后,我们对互斥量进行加锁和解锁,我们将使用pthread_mutex_lock和pthread_mutex_unlock。

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:成功返回0,失败返回错误号

注意:我们加锁只需要对临界区加锁,而且加锁的粒度越细越好。而加锁的本质是让线程执行临界区的代码串行化。

调用pthread_mutex_lock时,可能会遇到一下情况:

  • 互斥量处于未锁状态,该函数将会互斥量锁定,同时返回成功
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥锁解锁。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/syscall.h> //仅仅是了解

using namespace std;

int tickets = 10000; // 临界资源
pthread_mutex_t mutex;//定义锁
void *getTickets(void *args)
{
    const char *name = static_cast<const char *>(args);
    while (true)
    {
        //临界区
        pthread_mutex_lock(&mutex);
        if (tickets > 0)
        {
            usleep(1000);
            cout << name << " 抢到了票,票的编号是:" << tickets << endl;
            tickets--;
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            
            cout << name << " 已经放弃抢票了,因为没有票了....." << endl;
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_mutex_init(&mutex,nullptr);
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;

    pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");
    pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");
    pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");

    // 一旦分离不能join
    int n1 = pthread_join(tid1, nullptr);
    cout << "strerror(n1): " << strerror(n1) << endl;
    int n2 = pthread_join(tid2, nullptr);
    cout << "strerror(n2): " << strerror(n2) << endl;
    int n3 = pthread_join(tid3, nullptr);
    cout << "strerror(n3): " << strerror(n3) << endl;

    pthread_mutex_destroy(&mutex);
    return 0;
}

3.5.4 互斥锁的相关问题

  • 加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都需要加锁,不能有的线程加锁有的线程不加锁。
  • 锁保护的是临界区,任何线程执行临界区代码访问临界资源,都必须现申请锁,前提是都必须看到锁!那么这把锁本身也是临界资源!而锁的设计者也考虑了这个问题,pthread_mutex_lock线程竞争锁的过程,就是原子的!

(本篇完)

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

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

相关文章

MSF之ssh_login漏洞

ssh_login准备实操准备 目标机&#xff1a;windows xp 攻击机&#xff1a;kali 工具&#xff1a;metasploit framework 实操 先查看两机器的ip kali的ip为172.17.0.1 xp的ip为192.168.17.130 互相ping一下 没问题。 打开msf search ssh_login 爆出模块 use 0 show o…

vue项目打包流程与反向代理Nginx的使用

目录 前言 参考文章 正文 1.打包前的配置工作 做反向代理的原因&#xff08;Vue项目打包后Proxy失效的问题&#xff09;&#xff1a; 2.Nginx使用 前言 突发灵感想学习下打包&#xff0c;第一反应是学习webpack&#xff0c;翻找一通后发现用不着webpack&#xff0c;因为…

ORB-SLAM2 --- Tracking::UpdateLocalPoints函数

目录 1.函数作用 2.函数流程 3.code 4.函数解析 1.函数作用 更新局部关键点。先把局部地图清空&#xff0c;然后将局部关键帧的有效地图点添加到局部地图中。 2.函数流程 这是更新局部地图中的一个小函数&#xff0c;我们在Tracking::UpdateLocalKeyFrames更新了局部关键…

C++ Reference: Standard C++ Library reference: Containers: map: map: value_comp

C官网参考链接&#xff1a;https://cplusplus.com/reference/map/map/value_comp/ 公有成员函数 <map> std::map::value_comp value_compare value_comp() const;返回值比较对象 返回一个比较对象&#xff0c;该对象可用于比较两个元素&#xff0c;以获得第一个元素的键…

Python 基础| Python 直接赋值、深拷贝和浅拷贝

先看这三个词的意思我觉得菜鸟的总结就很好 Python 直接赋值、浅拷贝和深度拷贝 | 菜鸟教程 直接赋值&#xff1a;其实就是对象的引用&#xff08;别名&#xff09;。 浅拷贝(copy)&#xff1a;拷贝父对象&#xff0c;不会拷贝对象的内部的子对象。 深拷贝(deepcopy)&#xf…

采购过程中会遇到的四种风险!如何管理和控制?

采购风险通常是指采购过程可能出现的一些意外情况&#xff0c;这些情况都会影响采购预期目标的实现。采购风险通常是由管理不善引起的&#xff0c;本文解释了采购过程中会遇到的四种风险&#xff0c;并介绍通过正确实施8Manage SRM采购管理系统&#xff0c;可以有效管理和控制它…

说说Python程序的执行过程

1. Python是一门解释型语言&#xff1f; 我初学Python时&#xff0c;听到的关于Python的第一句话就是&#xff0c;Python是一门解释性语言&#xff0c;我就这样一直相信下去&#xff0c;直到发现了*.pyc文件的存在。如果是解释型语言&#xff0c;那么生成的*.pyc文件是什么呢&…

工程项目管理的特点

工程项目管理是一种只关注工程项目的项目管理。它使用与任何其他类型的项目管理相同的标准方法和流程。这种专业化可能会吸引任何想要进入项目管理领域的具有工程背景的人。 工程项目管理与工程管理 工程管理侧重于对具有以下特点的工程师和工程任务的管理&#xff1a; 1、…

java UDP通信程序DatagramSocket数据接收

在查看本文前 您可以先看看我的文章 java UDP通信程序DatagramSocket数据发送 对UDP有一个基本的了解 然后这里我们就直接看代码了 我们先创建一个包 包下创建两个类 分别是 sendOut 发送类 参考代码如下 import java.io.IOException; import java.net.DatagramPacket; impo…

【Flutter 笔记系列 第 3 篇】如何正确对待Name source files using `lowercase with underscores`

相信很多安装了一些提示插件的小伙伴都遇见过 Name source files using lowercase with underscores flutter 提示 如下图 此时会有两种选择 1.能跑就行&#xff0c;无视它 2.好烦&#xff0c;我也没干什么怎么就提示不规范了。 3.一定是哪里出了问题&#xff0c;我要找到…

高通Ride软件开发包使用指南(8)

高通Ride软件开发包使用指南&#xff08;8&#xff09;6.9跟踪6.10 基础感知延迟分析6.10.1生成CSV6.9跟踪 用户可以使用Google Chrome中的跟踪功能分析代码的执行浏览器以下步骤中的示例显示如何分析Foundation SDK相机知觉 必须启用相关日志条目&#xff0c;因为跟踪使用SD…

说说Spring事件发布机制

文章目录前言一、 使用到事件发布机制的源码二、Springboot启动过程中用到的部分事件三、Springboot中的监听器四、自定义事件源&#xff0c;事件监听器和事件发布器4.1 目录结构4.2 事件源4.3 事件监听器4.4 事件帮助器4.4.1 事件发布帮助接口4.4.2 事件发布帮助接口实现类4.4…

运维就业现状怎么样?技能要求高吗?

运维至少需要知道哪些知识才可以去面试&#xff1f; 有一个答案对这一话题的解读非常深入&#xff0c;体系化的分析了所需掌握的技术、工作内容、性质及可发展的方向等等&#xff0c;今天特别分享给大家&#xff0c;按照这个发展&#xff0c;运维则已不需担心就业、薪酬等问题了…

MySQL -2 指令

客户端SQL指令记录&#xff1a; -- 针对 数据库和针对数据表 &#xff08;一&#xff09;数据库 1. 查看当前所有数据库&#xff1a;show databases; 2. 创建数据库&#xff1a;create database 数据库名 DEFAULT CHARSET utf8 COLLATE utf8_general_ci; 3. 删除数据库&#…

【一建、一造经验分享】一建挺难的,要坚持才能得到

标签&#xff1a;【备考四年】、【2020全科通过一建】、【2021全科通过一造】 思绪回到2017年7月份&#xff0c;软考-项目管理师出成绩了&#xff0c;很幸运我通过了。由于通信行业及单位认可“以考代评”&#xff0c;所以我最先下手的是软考高级&#xff0c;拿证等同于高工。在…

程序员核心------详解调试(2)

所爱隔山海&#xff0c;山海皆可平&#xff0c;所念皆星河&#xff0c;星河不可及。 上课&#xff01; 接着上节课讲的调试&#xff08;1&#xff09;&#xff0c;本节课进一步讲解调试(2). 文章目录1.调试实例讲解&#xff08;2&#xff09;校招笔试题 2.如何写出好的&#…

【大数据技术Hadoop+Spark】MapReduce概要、思想、编程模型组件、工作原理详解(超详细)

MapReduce是Hadoop系统核心组件之一&#xff0c;它是一种可用于大数据并行处理的计算模型、框架和平台&#xff0c;主要解决海量数据的计算&#xff0c;是目前分布式计算模型中应用较为广泛的一种。 一、MapReduce核心思想 MapReduce的核心思想是“分而治之”。所谓“分而治之…

猿人学APP第一题

抓包分析 copy CURL 转 requests代码 def app1():import requestsheaders {"Accept-Language": "zh-CN,zh;q0.8","User-Agent": "Mozilla/5.0 (Linux; U; Android 8.1.0; zh-cn; Nexus 6P Build/OPM1.171019.011) AppleWebKit/534.30 (K…

KT6368A蓝牙芯片的MTU的说明以及如何加快BLE传输速率

目录 一、蓝牙MTU的简介 二、详细的方法说明以及测试 三、KT6368A提升ble传输速率方法 BLE传输带宽主要跟两个要素有关&#xff1a; 通信周期和每个通信点可传输的数据量。 1.通信周期&#xff1a; 安卓手机一般可支持到10ms&#xff0c;苹果一般可支持到15ms 2.每个通信点…

[附源码]Node.js计算机毕业设计高校实习管理平台系统Express

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我…