关于进程、线程、协程的小笔记。
进程控制块
操作系统为每个进程都建立了一个数据结构:进程控制块
进程控制块主要信息有
- 进程编号
- 进程状态
- 程序计数器
- 寄存器
- CPU 的调度信息
- 内存管理信息
- 打开文件列表
有了进程之后,CPU 就可以不再是批处理的方式,一个进程只能在 CPU 上运行一小会儿,然后马上切换到别的进程去执行
批处理:一个程序的执行必须等上一个程序完全执行完成才可以进入 CPU 执行
并发和并行
由于 CPU 运行速度超快,多个程序之间虽然在不断的切换运行,但在人类那缓慢的世界看来,就好像是同时在执行一样,这就是并发。
多年以后,多个程序真正的实现了并行:在豪华电脑中,每个程序都被分配了一个 CPU 核心,真正的同时执行
多核处理器是指在一个处理器上集成多个运算核心从而提升计算能力,也就是有多个真正并行计算的核心,每个处理核心对应一个内核线程。
现在的电脑一般是双核四线程、四核八线程,是采用超线程技术将一个物理处理核心模拟成两个逻辑处理核心,对应两个内核线程。
多线程
如果一个进程只有一个主线程,一遇到耗时的操作就得等待,界面就会假死。可以内部搞两个执行流程,一个用来读写硬盘,另一个处理界面操作。
于是一个进程中至少有一个执行流程(主线程),也可以开启新的执行流程(线程)
线程变成了最小的调度单位。进程反而变成了一个容器,也就是资源分配的最小单位
线程的生命周期
- 当线程的数量小于处理器的数量时,线程的并发是真正的并发,不同的线程运行在不同的处理器上。但当线程的数量大于处理器的数量时,线程的并发会受到一些阻碍,此时并不是真正的并发,因为此时至少有一个处理器会运行多个线程。
- 在单个处理器运行多个线程时,并发是一种模拟出来的状态。操作系统采用时间片轮转的方式轮流执行每一个线程。现在,几乎所有的现代操作系统采用的都是时间片轮转的抢占式调度方式,如我们熟悉的Unix、Linux、Windows及macOS等流行的操作系统。
- 线程是程序执行的最小单位,也是任务执行的最小单位。在早期只有进程的操作系统中,进程有五种状态,创建、就绪、运行、阻塞(等待)、退出。早期的进程相当于现在的只有单个线程的进程,那么现在的多线程也有五种状态,现在的多线程的生命周期与早期进程的生命周期类似。
协程
生产者消费者问题:用一个线程先去执行生产者,执行到特定位置就暂停,去执行消费者,然后特定位置暂停,去执行生产者……,这叫做合作式调度。和多线程的抢占式调度是不一样的。
两个协程你执行一会,我执行一会,不会同时执行,就不用对共享资源加锁了,也就不用互相通知了,代码写起来也简单多了。
协程有个重要的特点:完全被程序所调度和掌握,不关操作系统的事。
因为是自主开辟的异步任务,所以很多人也更喜欢叫它们纤程(Fiber),或者绿色线程(GreenThread)。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
协程的目的
- 在传统的J2EE系统中都是基于每个请求占用一个线程去完成完整的业务逻辑(包括事务)。所以系统的吞吐能力取决于每个线程的操作耗时。如果遇到很耗时的I/O行为,则整个系统的吞吐立刻下降,因为这个时候线程一直处于阻塞状态,如果线程很多的时候,会存在很多线程处于空闲状态(等待该线程执行完才能执行),造成了资源应用不彻底。
- 最常见的例子就是JDBC(它是同步阻塞的),这也是为什么很多人都说数据库是瓶颈的原因。这里的耗时其实是让CPU一直在等待I/O返回,说白了线程根本没有利用CPU去做运算,而是处于空转状态。而另外过多的线程,也会带来更多的ContextSwitch开销。
- 对于上述问题,现阶段行业里的比较流行的解决方案之一就是单线程加上异步回调。其代表派是node.js以及Java里的新秀Vert.x。
- 而协程的目的就是当出现长时间的I/O操作时,通过让出目前的协程调度,执行下一个任务的方式,来消除ContextSwitch上的开销。适用于被阻塞的,且需要大量并发的场景。但不适用于大量计算的多线程,遇到此种情况,更好使用线程去解决。
总结
多进程出现的原因
- 充分利用 CPU 的能力,遇到硬盘操作的时候,坚决不能让 CPU 等着,在那里空转,一定要切换到另外的程序上去
- 人类需要电脑『同时』运行多个程序
线程的出现主要是为了提高响应性,避免界面不响应的问题。对于服务端而言,多个请求发给了服务端,当一个请求在等待 IO 操作的时候,其他请求可以用别的线程来处理。但是线程多了,创建的开销,切换的开销也会很大,所以得考虑复用,形成线程池。
协程通过合作式调度,避免了多线程编程中各个各样烦人的,容易出错的问题:加锁、通知、阻塞
简单整理一下
- 进程:系统分配资源的最小单位
- 线程:CPU 时间片分配的最小单位
- 协程:在线程基础上的异步逻辑单位