进程、线程、I/O密集、计算密集
1. 进程和程序
程序只有被装在到内存中之后才能运行,而执行这个程序的主体就叫做进程。程序是一系列指令的集合,描述了进程应该如何执行这个程序,是静态的。进程是程序的一次执行活动,是动态的。
2. 多任务
线程和进程出现的目的都是为了实现多任务,只是对象层级不一样。进程是操作系统级别上的并发,线程是进程级别上的并发。
多任务实现的三种方式:
- 多进程
- 单进程+多线程
- 多进程+多线程
多进程的使用让单核 cpu 或者多核 cpu 并发处理命令成为可能。就比如早期的单核 cpu,虽然 cpu 执行代码都是顺序执行的,但是却利用多个进程之前的来回切换实现了伪多线程。因为速度足够快,用户基本无感,基本上满足了多任务的需求。
而如今的多核 cpu 基本上采用的是单进程中创建多个线程来执行程序,一台电脑可以开启很多个程序并发执行。多进程+多线程的程序相对较少;
3. 多进程的优缺点
优点:
- 健壮性更强,更加稳定;
缺点:
- 创建进程的代价大;
- 创建进程的数量有限,当大于 cpu 核数时仍然会出现等待的情况;
- 进程之间的通信消耗大,设计和实现复杂;
4. 多线程的优缺点
优点
- 线程的创建相对简单;
- 资源占用少;
- 线程之间的通信相对简单;
缺点
- 一个线程出问题可能导致整个程序出问题;
5. cpu最小调度单位
线程是 cpu 调度的最小单位。
也就是说进程可以被分配内存和资源,但是 cpu 的调度不是以进程为最小单位的。虽然进程调度器会在合适的时机为进程分配 cpu 时间,让其去执行程序的代码,但是进程最少有一个线程,也就是主线程,所以最终仍然是在线程中执行 cpu 操作。
6. 资源共享问题
进程中的线程的资源是共享的,线程一般不具备内存和资源。因此,在线程之间进行且换要比进程之间切换容易得多,消耗也会少很多。进程之间的通信需要使用的 ipc。
但是进程中的线程之间的内存和资源是共享的,如果一个线程中的内存使用发生错误,且这个内存会影响全局,那么程序就会崩溃。但是进程之间因为是相互隔离的,一个进程奔溃了,其他进程完全不会受影响。
所以,多进程程序会更加健壮,但是涉及到 IPC,程序的运行效率可能会稍微地下,早期的 Apach 服务器就是如此设计的。
7. I/O密集型和计算密集型
所有代码的执行本质上都是需要 cpu 来计算,唯一的区别就是有的计算很少,cpu花费的时间也就很少,比如 1+1 和计算圆周率。
所谓I/O操作是指内存与I/O设备之间进行信息交换。
I/O密集型任务以信息的交换为主,基本不消耗 CPU,因为 I/O 操作一般会委托中间人来进行,比如 DMA。所以,当 I/O 任务完成,需要使用到 cpu 时才调度 cpu 来执行代码,因为基本不需要计算,cpu 很快将代码执行完成然后就挂起等待下一次的召唤。所以 I/O 密集型任务执行期间,99%的时间都花在 I/O 操作上,花在CPU上的时间很少。因此,用运行速度极快的 C 语言替换用 Python 这样运行速度极低的脚本语言,提升的效率 = 语言提升的效率 * 1%,也就是完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
I/O密集型任务,任务数量越多使其不处于等待状态,就越能够利用 cpu。当然又有个限度,见后文。
计算密集型服务器收到的任务就不是 get 这么简单了,而是一个计算请求,如果涉及到内存、硬盘的访问,仍然是 i/o操作 + cpu 执行代码。但是因为这个计算很复杂,所以相对而言,I/O 操作所占用的时间比例就很小了。此时,最高效的使用 cpu 的任务数量是和 cpu 核数一样的任务数。这样每个任务独占一核,不需要切勿之间的切换,最高效的处理复杂计算任务。
此时会涉及到一个问题,就是编程语言的效果会直接决定 cpu 的计算速度。因为 c 语言更接近底层,说白了就是对cpu 的使用效率更高,这就决定了相同的计算请求,js首先需要对代码进行解释,解释完了之后可能还需要层层解剖之后才能最终走到汇编和 01 的二进制计算,中间的转换就消耗了很多时间。而 c 语言在这上面更有优势,编译完之后的代码本身就是二进制代码,省去了很多的中间步骤,对 cpu 的使用自然更加高效,最终的结果就是在计算密集型需求上,c 语言的优势随着计算密集性的增大而更大。因此 c/c++ 在计算复杂的领域有着不可替代的地位,比如大型游戏开发。而脚本语言比如 JS 则在计算需求很少的 Web 领域大放异彩。
8. 并发的限度
无论是多进程还是多线程,只要数量一多,效率肯定上不去,为什么呢?
我们打个比方,假设你不幸正在准备中考,每天晚上需要做语文、数学、英语、物理、化学这5科的作业,每项作业耗时1小时。
如果你先花1小时做语文作业,做完了,再花1小时做数学作业,这样,依次全部做完,一共花5小时,这种方式称为单任务模型,或者批处理任务模型。
假设你打算切换到多任务模型,可以先做1分钟语文,再切换到数学作业,做1分钟,再切换到英语,以此类推,只要切换速度足够快,这种方式就和单核CPU执行多任务是一样的了,以幼儿园小朋友的眼光来看,你就正在同时写5科作业。
但是,切换作业是有代价的,比如从语文切到数学,要先收拾桌子上的语文书本、钢笔(这叫保存现场),然后,打开数学课本、找出圆规直尺(这叫准备新环境),才能开始做数学作业。操作系统在切换进程或者线程时也是一样的,它需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。
所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。
9. 总结
- 进程是用来执行程序的;
- 线程是 cpu 最小调度单位;
- I/O 密集服务器更适合多任务,使用C/C++ 语言并不能提高性能;
- 计算密集服务器任务数最好是和核数相等,应该使用高性能的 C/C++ 语言;