进程 线程 纤程

进程和线程有什么区别?
在这里插入图片描述

答案:进程就是一个程序运行起来的状态,线程是一个进程中的不同的执行路径。
专业版:进程是OS分配资源的基本单位,线程是执行调度的基本单位。分配资源最重要的是:独立的内存空间,线程调度执行(线程共享进程的内存空间,没有自己独立的内存空间)

在这里插入图片描述

理论上,一个程序是可以启动多个进程的。例如,点击QQ,内存中创建一个进程。再点击一次,再创建一个。

不同的进程会被OS分配到一些资源。例如占有的寄存器和PC,不过最重要的资源是自己独立的内存地址空间。

进程到内存后,CPU开始读内存指令。把指令拿过来放到PC中,一条条执行。
进程放到内存后,main函数所在的地方,是启动点。那是我们的主线程。一旦开始执行后,一个进程里面可能有其他不同的线程,到底执行哪个线程,就需要CPU进行调度。

线程的实现

不同的操作系统,线程的实现不太一样。

在Linux里实现:
就是一个普通的进程,只不过与其他进程共享资源(内存空间,全局数据等)

在其它系统都有LWP的实现(轻量级进程 Light Weight Process)

从更高层面理解:一个进程中不同的执行路线,就叫做一个线程。

纤程(协程)

JVM跑在用户态,OS跑在内核态。JVM的线程和OS的线程是一一对应的。(即目前Hotspot目前是一对一)一般起一万个都很卡了。

Fiber:用户态的线程。线程里面的线程。在用户空间进行切换调度,不需要与OS打交道,只需要在自己的用户空间就可以完成。JVM自己管理,自己切换。起几万个,几十万个都没问题。
在这里插入图片描述

优势

  1. 占有资源很少 OS要起一个线程,首先1M内存就被占用了。 而Fiber只需要4K
  2. 切换比较简单
  3. 由于非常轻量级,可以启动很多个10W+

目前2020/3/22 自然而然支持内置纤程的语言:Kotlin Scala Go Python(加上某个lib)… Java?(open jdk : loom项目在做尝试)
JAVA在类库级别可以支持,不成熟

JDK14没有纤程

具体实现:相当于JVM自己实现了小小的OS级别的调度程序。

Java中对于纤程的支持:没有内置,盼望内置

利用Quaser库(不成熟)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.fibers.SuspendExecution;
import co.paralleluniverse.strands.SuspendableRunnable;

// 线程版 6秒左右
public class HelloFiber {

public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
Runnable r = new Runnable() {
@Override
public void run() {
calc(); // 做一个复杂计算
}
};
// 10000万个任务
// 在os系统级别起了1万多个线程(重量级切换)
int size = 10000;

Thread[] threads = new Thread[size];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(r);
}

for (int i = 0; i < threads.length; i++) {
threads[i].start();
}

for (int i = 0; i < threads.length; i++) {
threads[i].join();
}

long end = System.currentTimeMillis();
System.out.println(end - start);


}

static void calc() {
int result = 0;
for (int m = 0; m < 10000; m++) {
for (int i = 0; i < 200; i++) result += i;

}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import co.paralleluniverse.fibers.Fiber;
import co.paralleluniverse.fibers.SuspendExecution;
import co.paralleluniverse.strands.SuspendableRunnable;

// 纤程版 3秒左右
public class HelloFiber2 {

public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();


int size = 10000;
// void代表没有返回值
Fiber<Void>[] fibers = new Fiber[size];


// 纤程启动
for (int i = 0; i < fibers.length; i++) {
fibers[i] = new Fiber<Void>(new SuspendableRunnable() {
public void run() throws SuspendExecution, InterruptedException {
calc();
}
});
}

for (int i = 0; i < fibers.length; i++) {
fibers[i].start();
}

for (int i = 0; i < fibers.length; i++) {
fibers[i].join();
}

long end = System.currentTimeMillis();
System.out.println(end - start);


}

static void calc() {
int result = 0;
for (int m = 0; m < 10000; m++) {
for (int i = 0; i < 200; i++) result += i;

}
}
}

最好模型可以起10个线程,然后每个线程执行1000个任务。这样既利用了操作系统在内核级别的对线程的调度,又充分利用了JVM在用户空间对纤程的调度。

纤程的应用场景

纤程vs线程池:很短的计算任务,不需要和内核打交道(比如不需要读文件等),并发量高

进程

linux中也称task,是系统分配资源的基本单位。
资源包括:独立地址空间、内核数据结构(PCB…)、全局变量、数据段…
每一个进程有一个PCB(Process Control Block,进程描述符)
Linux管理时,把所有相应信息都记录到PCB表中。大小不固定

注:每一个线程也都有一个PCB

内核线程:内核启动后经常要做一些后台操作,比如计时,定期清理垃圾,这些由Kernel Tread完成。就是内核内部所使用的线程。

进程创建和启动

系统函数fork() exec()
内核暴露出来的接口,我们对其进行调用,来让内核帮我们干一些事情,就是系统函数。
从A中fork B,A为B的父进程。

僵尸进程 孤儿进程

僵尸进程:父进程产生子进程后,会维护子进程的PCB结构。子进程退出,由父进程释放。但如果父进程没有释放,那么子进程就成为一个僵尸进程。
一般情况没有什么影响。结束后相关资源已经都释放了,基本不再占资源,唯一占的资源就是PCB了。

C程序用wait()函数可以手动释放
kill掉父进程,就可以一起干掉僵尸进程

下面的例子就会产生僵尸进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>

int main() {
pid_t pid = fork();

if (0 == pid) { // 执行在子进程中
// getpid()获得该进程的编号
// getppid()获得该进程父进程的编号
printf("child id is %d\n", getpid());
printf("parent id is %d\n", getppid());
} else {
while(1) {}
}
}

孤儿进程:子进程结束前,父进程已经退出

一般情况下,会把孤儿进程都交给一个特殊进程(命令行底下一般是init进程 1号)处理。

影响不大。换了个爹。

下面的例子就会产生孤儿进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>

int main() {
// A fork 出一个B
pid_t pid = fork();

if (0 == pid) {
printf("child ppid is %d\n", getppid());
sleep(10);
printf("parent ppid is %d\n", getppid());// 此时已经换爹
} else {
printf("parent id is %d\n", getpid());
sleep(5);
exit(0); // 父亲A打印编号5s后死亡
}
}

执行结果:
child ppid is 19071
parent id is 19071
parent ppid is 1457

注:

  • 1457是UI界面(图形界面)孤儿的爹。如果UI界面都死了,那么就变成1号了
  • 1457的父亲是1

进程(任务)调度

内核进程调度器决定:
该哪个进程运行?
何时开始?
运行多长时间?

Linux中每个进程都可以指定不同的调度方案(可以自己写)

多任务
非抢占式(cooperative multitasking):除非进程主动让出CPU,否则将一直运行。(很少用)
抢占式(preemtive multitasking):由进程调度器强制开始或暂停(抢占)某一进程的执行。

进程调度
Linux2.5 经典Unix O(1)调度策略,偏向服务器,但对UI交互不友好。平均分配时间片,不能及时响应。

Linux2.6.23 采用CFS完全公平调度算法Completely Fair Scheduler
不再采用绝对时间片。因为有的进程不需要那么多时间。而是按优先级分配时间片的比例,记录每个进程的执行时间,如果有一个进程执行时间不到他应该分配的比例,优先执行(计算原来执行的时间片)

进程类型
IO密集型 大部分时间用于等待IO
CPU密集型 大部分时间用于闷头计算
(时间片先给CPU密集型,什么时候IO来了,马上给它执行)

进程优先级
实时进程 > 普通进程(099)
普通进程nice值(-20
19)

linux默认调度策略
对于实时进程:使用SCHED_FIFOSCHED_RR(Round Robin轮询)
对于普通进程:使用CFS

  • 其中等级最高的是FIFO。如果一个进程中一个优先级是99,一个是98。99的是先进先出,那么执行完了才轮到98.这种进程除非自己让出CPU,或者更高级的FIFO和RR抢占它。
  • 两个进程优先级一样的进程,使用RR策略。平均分配。
  • 只有实时进程主动让出,或执行完毕,普通进程才有机会运行。

(类似急诊和普通号。急诊中,有非常严重的,用FIFO,按严重程度顺序进行,除非前一个主动放弃,可以让出医生资源。不是特别严重的,严重程度相同,采用RR策略。急诊都结束,才进行普通号就诊,CFS策略)