欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JAVA程序员需要知道的计算机底层基础05-进程、线程、纤程(协程)

程序员文章站 2022-06-13 14:07:03
...

JAVA程序员需要知道的计算机底层基础05-进程、线程、纤程(协程)

每一个程序都可以启动多个进程。而每一个进程中会有若干个线程。

进程和线程有什么区别?

简单说:进程就是一个程序运行起来的状态,线程是一个进程中的不同的执行路径。

专业一点:进程是OS分配资源的基本单位,线程是执行调度的基本单位。
JAVA程序员需要知道的计算机底层基础05-进程、线程、纤程(协程)

分配资源最重要的是:会为每一个进程分配独立的内存地址空间。
JAVA程序员需要知道的计算机底层基础05-进程、线程、纤程(协程)

当进程被启动时,内存中会有一个main主线程启动,而其中可能会有多个线程,一旦程序执行时当CPU发生计算时,此时会由内存中某一个线程调度执行,因此说是线程是执行调度的基本单元。
一个进程内的所有线程共享进程的内存空间,没有自己独立的内存空间。

进程

JAVA程序员需要知道的计算机底层基础05-进程、线程、纤程(协程)

Linux使用一种数据结构PCB来代表着一个进程,通过某一个PCB来管理追踪某一个进程,一个进程都会有一个PCB进程描述符来代表。

进程创建和启动

JAVA程序员需要知道的计算机底层基础05-进程、线程、纤程(协程)

系统函数,就是linux通过c编写的函数,并对外暴露接口。

通过调用fork生成一个新的进程,通过调用exec,来运行这个进程。

fork本质是调用了clone,clone是从当前进程clone出一个新的进程出来。

僵尸进程

JAVA程序员需要知道的计算机底层基础05-进程、线程、纤程(协程)

每个进程都有一个PCB结构,而当一个父进程通过fork,生出几个子进程之后,这个父进程将会维护每一个子进程的PCB结构,当子进程退出的时候,需要父进程对这个PCB结构进行手动释放(通过内核系统调用的wait函数来释放),因为子进程自身无法释放自己的PCB,如果不释放,那此时退出的子进程就是一个僵尸进程。
一般造成僵尸进程的原因是父进程可能是一个在后台运行的daemon线程,编写的时候不小心没有进行释放。
单独一个僵尸进程理论来说不会对系统造成什么太大影响,因为子进程退出,所持有的资源已经被释放,只剩下一个空壳结构,只占用少量空间。 但如果这个数量累积的非常多,实际还是会对系统造成影响,因此实际编写C程序时应切记手动释放PCB。

在linux中通过ps,如果发现带有的进程,那这个就是一个僵尸进程
JAVA程序员需要知道的计算机底层基础05-进程、线程、纤程(协程)

如果你尝试利用kill命令对僵尸进程进行强制终止,会发现并不会达到你的预期,因为僵尸进程本身已经是一个死了的进程,此时只有对父进程杀死后,这个僵尸进程才会消失。

代码实现:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
//模拟僵尸进程​
int main() {
        //通过fork调用生成一个子进程, 返回的pid就是子进程的id号
        pid_t pid = fork();
        
        //新生成的子进程也会执行下面的代码,而它的pid变量则是0,因此这里是为了逻辑只在子进程里运行​
        if (0 == pid) {
                //获得当前进程(即子进程)的进程id号
                printf("child id is %d\n", getpid());
                //获得当前进程(即子进程)的父进程id号
                printf("parent id is %d\n", getppid());
        } else {
                //父进程会执行这个逻辑,因此会永不停止,而子进程执行完上面的if里的语句后,就退出了,此时父进程并未释放子进程的PCB,因此子进程此时就是僵尸进程
                while(1) {}
        }
}

孤儿进程

JAVA程序员需要知道的计算机底层基础05-进程、线程、纤程(协程)

因为父进程管理着子进程的PCB,而这个PCB释放不能由子进程自身进行释放,必须要由父进程进行释放。
当子进程还在运行的时候,父进程就退出的情况下,此时这个子进程就变成了一个孤儿进程,然后操作系统会将它托管给一个新的父进程(这个父进程一般是提前预定义好的,是作为所有孤儿进程的父进程),这个父进程一般是init进程,接下来将由这个新的父进程进行管理。 因此实际上孤儿进程不会对系统造成什么影响,只是换了一个新的托管所。

代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
//模拟孤儿进程​,  父进程睡5秒就结束, 子进程睡10秒,然后在睡的前后打印它的父进程id,应该是不同的。
int main() {
         //通过fork调用生成一个子进程, 返回的pid就是子进程的id号
        pid_t pid = fork();
        //新生成的子进程也会执行下面的代码,而它的pid变量则是0,因此这里是为了逻辑只在子进程里运行​​
        if (0 == pid) {
                //获得当前进程(即子进程)的父进程id号
                printf("child ppid is %d\n", getppid());
                sleep(10);
                //获得当前进程(即子进程)的父进程id号,此时的pid应该与上面的pid是不同的,因为它上一个父进程已经结束了。
                printf("parent ppid is %d\n", getppid());
        } else {
                //打印当前进程的id号
                printf("parent id is %d\n", getpid());
                sleep(5);
                exit(0);
        }
}

线程

JAVA程序员需要知道的计算机底层基础05-进程、线程、纤程(协程)

每个操作系统对线程的实现都不相同。
JAVA程序员需要知道的计算机底层基础05-进程、线程、纤程(协程)

对linux而言,首先启动一个进程就是执行一个系统调用fork函数,那么就会生成一个新的进程,这个进程也叫做执行系统调用进程的子进程。
而从linux操作系统的角度来说,启动一个线程,实质上也是通过fork调用生成了一个新的子进程,只不过此时这两个父子进程之间共享同一块内存空间。

内核线程

JAVA程序员需要知道的计算机底层基础05-进程、线程、纤程(协程)

有一些只供内核使用的线程,来在后台完成一些操作,需要注意的是这种线程与用户态申请的内核态生成的传统线程并不相同,内核线程仅仅是为了帮助进行一些只属于内核相关的工作。

纤程/协程(Fiber)

JAVA程序员需要知道的计算机底层基础05-进程、线程、纤程(协程)

纤程和协程是一个东西,现在也没有统一的中文定义。
对于Hotspot JVM的线程来说,是与操作系统一一对应的,因此免不了大量的用户态切内核态去调度系统级别的线程。
而纤程是用户态上的线程,是线程中的线程,切换和调度不需要经过OS,因此是非常轻量级的。

纤程的优势

1、占有资源很少, 正常来讲,对于OS的线程来说,整个线程大概需要耗费1M的内存空间,而Fiber则只需要4K的内存空间 ,因此可以启动非常多的纤程(10w+一点问题也没有)
2、切换比较简单,无需系统级别调度,只需在用户态程序内部实现一个轻量级的操作系统调度即可(例如可以通过栈来存储每个纤程的状态)。

目前支持内置纤程的语言

Kotlin、Scala、Go、Python(通过第三方lib)。。。
Java支持吗?从目前的JDK14来说,语言级别并未支持,但存在研究纤程方向的分支(open jdk : loom),但到JDK14为止仍未合并,有望在后续版本支持 。 但目前可以通过第三方类库进行实现

java利用Quaser库实现纤程(不成熟)

使用实例参照之前写过的:
线程之上,纤程.note

纤程的最佳模型

一个线程可以对应多个纤程,而为了最大效率的提高效率,我们可以在纤程的基础上,合理最大利用硬件资源,可以启用多个线程的基础上,每个线程再分别启动多个纤程。

纤程的应用场景

1、对于一些很短的计算任务,不需要和内核打交道(例如计算任务中不存在一些io操作,如果需要io操作,让N多纤程都一同在等着就不合适了)。
2、在面对并发量高的时候,纤程非常的合适。