【1++的Linux】之线程(一)

news2025/1/12 6:45:36

👍作者主页:进击的1++
🤩 专栏链接:【1++的Linux】

文章目录

  • 一,Linux线程概念
  • 二,线程的优缺点
    • 进程和线程类比现实
  • 三, 线程的操作
    • 线程的私有资源 && 线程的创建
    • 线程的等待
    • 线程终止
    • 线程分离
  • 四,如何理解线程id

一,Linux线程概念

1. 什么叫做线程?

在官方的定义中:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
也就是说,线程在进程中运行,是OS调度的基本单位。

2. 类比进程对线程再做理解

线程在进程中运行,所以一个进程中可能有多个线程,那么要不要将这些线程管理起来???答案是要的,所以,先描述,再组织。用特定的数据结构将线程管理起来,在一般的OS系统中,我们将这种结构称为TCB,但是Linux中却不是这样实现的。

我们分多个视角来看Linux下的解决方案:
CPU视角: 我们的CPU只认PCB结构体,我拿到一个PCB结构体,我就执行其所对应的代码和数据。我并不关心这是进程还是其他所谓的定义给用户的一些概念。
我们以前所 认识的进程都是只有一个执行流的进程,其内部也可以有多个执行流,这每个执行流我们其实就可以称为线程。我们上面说过,我们的CPU只认PCB,也可以说是,CPU认为一个PCB就是一个执行流。那么每个线程就就需要一个 PCB来进行管理,在其他的一些系统中,这个PCB叫做TCB,但在Linux中,不是这样做的,Linux认为,既然你的大多是结构都和进程类似,那么我可不可以就把你当作一个" 进程 "呢?答案是可以的,这样我就可以简化我OS的复杂度。

那么怎么将程当作一个线程呢?
我们再来回顾以下线程的概念:线程在进程中执行,也就是说,线程在进程的地址空间中运行,一个执行流我们其实就可以称为一个线程。那么我们只需在创建一个特殊的进程,能够与进程共享地址空间既可以,再 通过特殊的手段将当前进程的资源分配给这些特殊的进程,我们将这种特殊的进程就可以称作线程。
在这里插入图片描述
在CPU视角,Linux下PCB<=其他OS下的PCB 为什么呢?
因为linux并没有为线程准备特定的数据结构。在内核看来,只有进程而没有线程,在调度时也是当做进程来调度。linux所谓的线程其实是与其他进程共享资源的进程。为什么说是轻量级?在于它只有一个最小的执行上下文和调度程序所需的统计信息。他是进程的执行部分,只带有执行相关的信息。

内核的视角: 进程是分配系统资源的实体,那么线程就是承担进程一部分资源的实体,进程划分资源给线程。

我们的CPU所调度的实际上是线程,因此线程也是CPU的基本调度单位

因此Linux下的进程统一称为:轻量级进程

Linux下并没有真正意义上的线程,而是用进程的PCB去模拟实现线程。我创**建一个线程和我的父进程挂接到同一份地址空间上,然后在分配资源,分配完还要管理,管理完进行释放。**所以对用户特别不友好!这就是linux用进程去模拟线程的缺点。所以OS并不能直接给我们线程相关的接口,只能提供轻量级进程的接口。但是我们普通用户使用起来是有难度的,因此在用户层又实现了一套用户级的线程接口(以第三方库的形式)。

linux中vfork系统调用,它的作用就是创建一个进程,但是这个进程和父进程共享地址空间。

二,线程的优缺点

优点

  1. 创建一个新线程的代价要比创建一个新进程小得多
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  3. 线程占用的资源要比进程少很多
  4. 能充分利用多处理器的可并行数量
  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

下面我们对上面的某些观点进行解释:

为什么说创建一个新的线程代价要比进程小的多: 因为进程之间是独立的,创建一个新进程,就必须要创建新的地址空间,页表,并将代码和数据进行映射。而创建一个线程,我们只需要创建一个新的PCB,并将对应进程中的部分资源划分给线程。

为什么说与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多: 我们的执行流在切换时不仅仅是把在CPU和寄存器中的数据(也就是当前线程的上下文数据)切下来,再把新的线程的数据和代码拿上去跑。我们的计算机为了提高效率,在内存和CPU之间还有多级缓存,从内存中读取数据时,并不是只读自己想要的部分。而是读取足够的字节来填入缓存中。
在这里插入图片描述
在切换线程时,由于我们的线程之间是共享地址空间的,因此多级缓存中的东西就不需要去切换,但是在切换进程时,我们的进程又自己独立的进程地址空间,因此缓存中的东西必须都得切换掉。

缺点

性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多

性能损失具体是指什么:
若我们有一个非常大的计算任务分给多个线程去做,且我们的线程要比可用的处理器要多,此时我们的某个线程执行到一般被切了下去,让另一个线程去执行,此时就会比让这个线程一直执行计算任务效率低的多。

进程和线程类比现实

说了这么多进程和线程的概念,那么我们可不可以将进程和线程类比现实中的东西呢?答案是—可以的!!!

我们将OS类比这个社会,在这个社会中承担分配资源的实体是一个个家庭,每个家庭都有自己的房子,家庭和家庭之间互相独立,偶尔可以串门进行通信,家庭里有一个个成员,大家各司其职,为了过更好的日子而努力。一个家庭就是一个进程,家庭中的各个成员就好比线程。在这个家庭中,沙发,电视机,冰箱等都是共享的,我们每个成员都可以使用,但是也有一些你自己私有的东西,其他成员是不能看的。
线程出现的目的是为了提高我们的效率,当一个执行流阻塞时,另一个执行流可以接着干其他的事情,还可以是,有多个处理器时我们的多执行流可以同时运行。就好比一个家庭中,一个认在做饭,另一个人可以去打扫卫生,而不是让一个人做完饭再去打扫卫生,这样效率就非常低了。

三, 线程的操作

线程的私有资源 && 线程的创建

由于线程在进程中执行,线程和进程是共享地址空间的,所以其大部分资源都是可以共享的,但也有部分资源是私有的。

  1. 栈:体现的就是每个线程在运行形成的临时数据是可以被压栈入栈的,线程和线程之间临时数据不会互相干扰。
  2. 上下文(一组寄存器):调度上下文,因为一个线程是调度的基本单位,所以一定会形成自己在CPU寄存器中的临时数据,线程是调度的基本单位,必须要有独立的上下文。
  3. 线程ID
  4. errno
  5. 信号屏蔽字
  6. 调度优先级

下面我们对上述的部分说法进行验证:
在这里插入图片描述
该函数是用户级的线程库给我们提供的一个创建线程的接口,

一个新的线程,在编译链接的时候需要引入pthread这个库 。
第一个参数:创建线程的id
第二个参数:线程属性
第三个:线程的回调属性,意味着你要让你的线程执行你代码的哪一部分
第四个参数:给这个回调函数传入的参数

返回值
在这里插入图片描述
我们来看一段代码:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cassert>

using namespace std;

void* thread_new(void* args)
{
    while(true)
    {
        cout<<"I am new thread"<<endl;
        sleep(1);

    }
}

int main()
{
    pthread_t id;
    int n=pthread_create(&id,nullptr,thread_new,(void*)"thread 1");
    while(true)
    {
        cout<<"I am main thread"<<endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
在这里插入图片描述
我们可以看到确实有两个执行流在运行。ps -aL可以查看轻量级进程。LWP是轻量级进程的id。
我们可以看到其线程pid确实是私有的。我们还可以观察到,其进程pid是一样的,并且有一个线程pid和进程pid是一样的,该线程我们叫做主线程。

谁调用这个函数,就获取谁的线程id。
在这里插入图片描述

void* thread_new(void* args)
{
    while(true)
    {
        cout<<"I am new thread"<<"我的id是"<<pthread_self()<<endl;
        sleep(1);

    }
}

int main()
{
    pthread_t tid;
    int n=pthread_create(&tid,nullptr,thread_new,(void*)"thread 1");
    while(true)
    {
        cout<<"I am main thread"<<"我创建的线程id "<<tid<<endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
线程的健壮性是有问题的,接下来我们进行验证:

void* thread(void* args)
{
    if(args==(void*)4)
    {
        int count=3;
       while(count--)
       {
        sleep(1);
       }

        //除0
        int a=1;
        a/=0;
    }
    else
    {
        while(true)
        {
            cout<<"I am new thread"<<" id :"<<pthread_self()<<endl;
            sleep(1);
        }
    }

}
int main()
{
    pthread_t  tid[5];
    for(int i=0;i<5;i++)
    {
        pthread_create(tid+i,nullptr,thread,(void*)i);
    }

    while(true)
    {
        cout<<"............................."<<endl;
        cout<<"I am main thread"<<endl;
        cout<<"............................."<<endl;

        sleep(1);

    }

    return 0;
}

在这里插入图片描述

我们发现程序在三秒后就因为除0错误而退出了,这也说明只要有一个线程出问题,整个进程都得要退出。也说明了线程的健壮性不强。

线程的等待

和进程一样,线程也是需要主线程等待的,否则就会形成僵尸,从而造成内存泄漏。

在这里插入图片描述

第一个参数: 你要等那个线程,传的是线程id。
第二个参数:输出型参数,用来获取新线程退出的时候,函数的返回值。因为你的线程执行函数的返回值是void*,我要以参数的形式把你的返回值拿出来,我就必须是void**。

一个执行流的执行结果有三种情况:代码跑完,结果正确,代码跑完,结果不正确,异常。前两种情况我们在前面学习进程中可以以退出码的形式观察到。但在线程中我们使用的是返回值。

下面是相关代码演示:

void * thread(void* args)
{
    int count=5;
    while(count--)
    {
        sleep(1);
        cout<<"I am new thread"<<endl;
        sleep(1);
    }

    return (void*)3;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,thread,(void*)"thread 1");
    void* retval=nullptr;
    pthread_join(tid,&retval);
    cout<<retval<<endl;

    return 0;
}

在这里插入图片描述

我们发现主线程确实接收到了新线程的返回值,并且是阻塞式等待。

那么对于代码异常这种情况,pthread_join能或者需要处理吗?

根本不需要,因为线程出现异常是进程的问题。线程出现问题,主线程根本管不了。某个线程出现问题退出了,那么整个进程就会退出。

线程终止

  1. 从线程函数return.(a.main函数退出return的时候代表进程退出(进程退出又叫主线程退出)b.其他线程函数return ,只代表当前线程退出)
  2. 线程可以调用pthread_ exit终止自己。
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
    在这里插入图片描述
    在这里插入图片描述
    下面是相关代码演示:
void * thread(void* args)
{
    int count=5;
    while(count--)
    {
        sleep(1);
        cout<<"I am new thread"<<endl;
        sleep(1);
    }
    pthread_exit((void*)3);

    //return (void*)3;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,thread,(void*)"thread 1");
    void* retval=nullptr;
    pthread_join(tid,&retval);
    cout<<retval<<endl;

    return 0;
}

在这里插入图片描述

void * thread(void* args)
{
    int count=5;
    while(count--)
    {
        sleep(1);
        cout<<"I am new thread"<<endl;
        sleep(1);
    }
   // pthread_exit((void*)3);

    return (void*)3;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,thread,(void*)"thread 1");
    void* retval=nullptr;
    while(1)
    {
        sleep(3);
        pthread_cancel(tid);
        sleep(2);
        break;
    }
    
    return 0;
}

在这里插入图片描述
在这里插入图片描述

我们可以观察到新创建的进程确实退出了,并且退出码位-1。

这里的-1就是PTHREAD_CANCELED。所以以后我们发现一个线程的退出码是-1,就证明当前线程是被取消的 。

线程有程序替换吗?
线程也可以调用程序替换,但是多线程中所有的代码和数据都是被线程共享的,如果其中有一个线程执行了程序替换,就直接影响到了其他的线程,所以在大部分情况下,很少让线程去调用程序替换,除非你让线程创建子进程再程序替换。一般程序替换和进程强关联。所以不考虑线程的程序替换。

线程分离

线程有程序替换吗?
以上等待线程是阻塞等待,如果我们不想等呢?
线程分离,分离之后的线程不需要被join,运行完毕之后,会自动释放Z状态的pcb,不需要我们等了。这个功能类比于进程中signal(SIGCHLD,SIG_IGN)直接忽略掉。所谓的分离只是设置线程的一种状态表示它不需要被等。就像我们在前面进行的类比,将进程比作一个家庭,线程是家庭中的一个成员,线程分离好比,你仍然在这个家住,但没人在去管你,但是,你要是出现异常退出了,那么整个家也会受到影响。

在这里插入图片描述

下面进行演示:

void * thread(void* args)
{
    pthread_detach(pthread_self());
    int count=5;
    while(count--)
    {
        cout<<"I am new thread"<<endl;
        sleep(1);
    }

    return (void*)3;
}

int main()
{
    pthread_t tid;
    int g_id=pthread_self();
    pthread_create(&tid,nullptr,thread,(void*)"thread");
    void* retval=nullptr;

       cout<<"I am main thread"<<endl;
      sleep(1);
 
     int n=pthread_join(tid,&retval);
     printf("retval:%d---n:%d\n",retval,n);
    return 0; 
}

在这里插入图片描述
我们可以看到join的返回值不为0,则表示等待失败。

一个线程被设置为分离后,绝对不能在进行join了。使用场景:主线程不退出,并且主线程不关心新线程的返回结果,新线程处理业务处理完毕后自行退出。

四,如何理解线程id

我们会发现线程id和LWP是不一样的,这是为什么呢?
我们查看到的线程id是pthread库的线程id,不是linux内核中的LWP,pthread库的线程id是一个内存地址。这个内存地址是一个虚拟地址。

那么我们又该如何理解线程id呢?
线程在进程中运行,在进程中通过pthread_create创建一个新的进程,这个函数是谁提供的?–是我们的第三方库pthread库提供的,这个库在磁盘中,当我们要使用时,其被加载到内存中,被映射到当前进程的地址空间中的共享区,若有多个进程想要访问,我物理内存中只需要有一份,就都映射到自己的共享区就可以了。
每个线程在运行时,都会有自己的上下文数据,因此,每个线程都需要自己的私有栈来保存这些上下文数据,我的线程要被管理,虽然线程状态等信息在内核的LWP中,但在用户层也要获得相关的属性,这些属性就要放在栈中,但我们用户级栈只有一个,不可能让多个线程去共享,这样使得数据混乱或者被覆盖。这个栈是主线程私有的栈。那么新线程的栈从哪里来呢?我们的用户级线程是由pthread库帮我们创建的,那么新线程的栈也应由它来为我们维护,这个库在我们的共享区,其就会在共享区为我们开辟新线程私有的栈,我们只需要拿到这个栈在地址空间的起始地址,就可以访问这个栈中的代码和数据了。因此将这个虚拟地址作为了我们用户层面所看到的线程id。它与内核中的LWP是一 一对应的。
这叫做用户级线程1:1式的和内核轻量级进程进行1:1对应,这就是linux实现线程的方案。

在这里插入图片描述

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

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

相关文章

初学Flutter:swiper实现

效果展示&#xff1a; flutter swiper 1、安装 card_swiper 2、引入card_swiper import package:card_swiper/card_swiper.dart;3、使用 这里我主要是对官网例子进行实践&#xff0c;主要是5种常用的swiper 1、普遍的swiper //custom swiper class CustomSwiper extends S…

下载安装各种版本的Vscode以及解决VScode官网下载慢的问题

下载指定版本 在Vscode官网 Vscode官网更新子页 这里的左侧栏点击其中一个会跳转到某个版本&#xff0c;或者在官网子页 https://code.visualstudio.com/updates的后面跟上需要的版本号即可完成目标版本下载页面的跳转 选择Linux里的ARM包不会自动下载而是跳转到另一个页面 …

vue-admin-template 安装遇到的问题

vue-element-admin 是一个后台前端解决方案&#xff0c;它基于 vue 和 element-ui实现。 参考文档&#xff1a; 官网&#xff1a; https://panjiachen.github.io/vue-element-admin-site/zh/guide/#%E5%8A%9F%E8%83%BD遇到的问题&#xff1a; npm ERR! Error while executing…

移动硬盘怎么加密?移动硬盘加密怎么设置?

在工作中&#xff0c;我们经常需要使用移动硬盘来保存重要数据&#xff0c;但是这样却不能保护重要数据的安全。所以&#xff0c;我们可以使用加密来保护移动硬盘。那么&#xff0c;移动硬盘要怎么加密呢&#xff1f; U盘超级加密3000 U盘超级加密3000是一款专业的移动储存设备…

Java类加载机制(类加载器,双亲委派模型,热部署示例)

Java类加载机制 类加载器类加载器的执行流程类加载器的种类加载器之间的关系ClassLoader 的主要方法Class.forName()与ClassLoader.loadClass()区别 双亲委派模型双亲委派 类加载流程优缺点 热部署简单示例 类加载器 类加载器的执行流程 类加载器的种类 AppClassLoader 应用类…

利用maven的dependency插件分析工程的依赖

dependency:analyze https://maven.apache.org/plugins/maven-dependency-plugin/analyze-mojo.html 分析项目的依赖&#xff0c;确定哪些&#xff1a;用了并且声明了、用了但没有声明、没有使用但声明了。 dependency:analyze可以单独使用&#xff0c;所以它总是会执行test-…

【算法练习Day38】零钱兑换完全平方数

​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;练题 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 文章目录 零钱兑换完全平方数总结&am…

软件测试:单元测试、集成测试、系统测试详解

实际的测试工作当中&#xff0c;我们会从不同的角度对软件测试的活动进行分类&#xff0c;题主说的“单元测试&#xff0c;集成测试&#xff0c;系统测试”&#xff0c;是按照开发阶段进行测试活动的划分。这种划分完整的分类&#xff0c;其实是分为四种“单元测试&#xff0c;…

TablePlus for Mac 数据库管理工具功能介绍

TablePlus是一款功能强大的数据库管理工具&#xff0c;专为Mac操作系统设计。它支持多种主流数据库&#xff0c;包括MySQL&#xff0c;PostgreSQL&#xff0c;SQLite&#xff0c;Microsoft SQL Server&#xff0c;Amazon Redshift等等。无论您是开发人员、数据库管理员还是数据…

【2024最新】Android Debug Bridge【下载安装】零基础到大神【附下载链接】

一、ADB简介 1、什么是adb ADB 全称为 Android Debug Bridge&#xff0c;起到调试桥的作用&#xff0c;是一个客户端-服务器端程序。其中客户端是用来操作的电脑&#xff0c;服务端是 Android 设备。 ADB 也是 Android SDK 中的一个工具&#xff0c;可以直接操作管理 Androi…

多个PDF发票合并实现一张A4纸打印2张电子/数电发票功能

python教程79--A4纸增值税电子发票合并打印_python 打印 发票设置_颐街的博客-CSDN博客文章浏览阅读7.9k次。接上篇https://blog.csdn.net/itmsn/article/details/121902974?spm1001.2014.3001.5501一张A4纸上下2张增值税电子发票实现办法。使用环境&#xff1a;python3.8、ma…

【C++基础知识学习笔记】精华版(复习专用)

常用语法 函数重载(Overload) 规则: 函数名相同 参数个数不同、参数类型不同、参数顺序不同 注意: 返回值类型与函数重载无关 调用函数时,实参的隐式类型转换可能会产生二义性 默认参数 C++ 允许函数设置默认参数,在调用时可以根据情况省略实参。规则如下: 默认参数只能…

chinese_llama_aplaca训练和代码分析

训练细节 ymcui/Chinese-LLaMA-Alpaca Wiki GitHub中文LLaMA&Alpaca大语言模型本地CPU/GPU训练部署 (Chinese LLaMA & Alpaca LLMs) - 训练细节 ymcui/Chinese-LLaMA-Alpaca Wikihttps://github.com/ymcui/Chinese-LLaMA-Alpaca/wiki/%E8%AE%AD%E7%BB%83%E7%BB%86%E…

selenium自动化测试入门 —— 获取元素对象!

一、元素定位简介 八种属性定位页面元素&#xff1a; By.ID By.XPATH By.LINK_TEXT By.PARTIAL_LINK_TEXT By.NAME By.TAG_NAME By.CLASS_NAME By.CSS_SELECTOR webdriver元素定位方法&#xff1a; driver.find_element(By.XXX,元素属性) # 定位单个元素 driver.find_elemen…

按键精灵中常用的命令

1. 声明变量&#xff1a; Dim 2. 注释语句 (1). 单行注释&#xff1a;这是一行注释&#xff0c;使用一个单引号进行注释&#xff1b; (2). 单行注释&#xff1a;// 这是一行注释&#xff0c;使用一对反斜杠进行注释; (3). 多行注释&#xff1a;/*这是多行注释&#xff0c;中…

canal+es+kibana+springboot

1、环境准备 服务器&#xff1a;Centos7 Jdk版本&#xff1a;1.8 Mysql版本&#xff1a;5.7.44 Canal版本&#xff1a;1.17 Es版本&#xff1a;7.12.1 kibana版本&#xff1a;7.12.1 软件包下载地址&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1jRpCJP0-hr9aI…

基于野狗算法的无人机航迹规划-附代码

基于野狗算法的无人机航迹规划 文章目录 基于野狗算法的无人机航迹规划1.野狗搜索算法2.无人机飞行环境建模3.无人机航迹规划建模4.实验结果4.1地图创建4.2 航迹规划 5.参考文献6.Matlab代码 摘要&#xff1a;本文主要介绍利用野狗算法来优化无人机航迹规划。 1.野狗搜索算法 …

前端面试题整理(一)

前言&#xff1a; 这篇博客是记录自己在看面试过程中还未完全掌握的前端知识点&#xff0c;也是一些前端面试需要掌握的知识点&#xff08;总结的并不全面&#xff0c;可以参考&#xff0c;具体情况以自己实际为准&#xff09;,并且这篇博客正在持续更新中… 附言&#xff1a…

2023.11.4-Envoy使用案例-oss

2023.11.4-Envoy使用案例 目录 本节实战 实战名称&#x1f6a9; 实战&#xff1a;前端代理-2023.11.2(测试成功)&#x1f6a9; 实战&#xff1a;流量镜像-2023.11.4(测试成功)&#x1f6a9; 实战&#xff1a;故障注入过滤器-2023.11.4(测试成功)&#x1f6a9; 实战&#xff1a…

【Invea Therapeutics】申请7500万美元纳斯达克IPO上市

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 猛兽财经获悉&#xff0c;美国生物制药公司【Invea Therapeutics】近期已向美国证券交易委员会&#xff08;SEC&#xff09;提交招股书&#xff0c;申请在纳斯达克IPO上市&#xff0c;股票代码为(INAI) &#xff0c;Invea …