从计算机的发展历史聊进程、线程、协程

进程的出现

如果要给进程下一个定义, 可以说是指在操作系统中能够独立运行,并且作为资源分配的基本单位。它表示运行中的程序。
我们知道,计算机出现之初,只是为了解决数学计算问题,计算机开始都是单CPU,单进程的。我们让计算机从某种输入源中读取数据,进行计算,并进行输出,执行完成后再进行下一个任务,这种实现方式是最简单,最朴素的实现方式,当输入数据量不大时,能够使CPU一直处于计算状态,合理的利用CPU。
但是可以想象,由于数据的读取过程是阻塞的,计算机进行IO操作时CPU处于闲置状态,当IO时间占比较大时,CPU的计算资源会被严重浪费,那么如何解决这个问题呢,前辈们的第一个解决方案是多道程序设计
多道程序设计就是把内存分为几个部分,每一个部分放不同的程序。当一个程序需要等待I/O操作完成时。那么CPU可以切换执行内存中的另外一个程序。如果内存中可以同时存放足够多的程序,那CPU的利用率可以接近100%。这时进程的概念就出现了,进程的本质是一个正在执行的程序,程序运行时系统会创建一个进程,并且给每个进程分配独立的内存地址空间保证每个进程地址不会相互干扰。
同时,在CPU对进程做时间片的切换时,保证进程切换过程中仍然要从进程切换之前运行的位置出开始执行。所以进程通常还会包括程序计数器、堆栈指针。此外,在进行进程切换时,我们同样也要保存好切出进程的硬件上下文(寄存器状态),并读入切入进程的硬件上下文。
多进程的程序设计基于这种设计思路就不断地发展了下去,对于进程间的轮转切换我们不断完善了进程的调度方式,对于进程的内存分配我们发展出了各种内存管理方式(块式,段式,页式,段页式)和虚拟内存的概念。

为什么需要线程

由于每个进程有着自己所独立的内存空间,因此当一个进程A阻塞后,系统调度使用其他的进程,是无法访问进程A的资源的,这样在一些场景下会非常不方便,比如word以多进程方式实现,我们编辑word时,word无法进行后台的自动保存,即使使用fork的方式复制父进程的资源,这也是对存储资源的极大浪费,那么有没有更好的解决方式呢?我们可不可以创建一种轻量级的进程,某些轻量级的进程共享他们的内存空间呢?
粒度更细的线程就因此而出现了,一个进程可以创建很多个线程来执行任务,没有了进程的界限区分,CPU会在线程之间来回切换的工作,共享相同的内存资源。这样带来的另一个好处是线程的不再涉及内存地址空间的切换,无需像进程的切换去内存中重新读取页表到cache中,因此切换的效率更高。
实际上发展到今天,进程已经渐渐演变成了一个“容器”的角色,而线程往往是我们进行调度和分派的基本单位。

协程的出现

在动辄万级并发的今天,即使是比进程轻量级的多的线程,也显得太笨重了。每个线程都需要维护自己的TLB和堆栈,默认一个线程的创建需要1MB的存储空间(取决于实现),而单机4GB的内存仅能创建4096个线程,同时由于多线程高并发下对资源的争夺导致的时间消耗,使得线程在一些场景下效率并不高。
这时协程就应运而生了,协程的本质是用户态的线程,他对内核是不可见的,因此协程的切换无需操作系统介入,不会涉及用户态和内核态的切换。一个线程可以创建多个协程,同属于一个线程的多个协程不会同时执行,因此省去了加锁的烦恼。协程使用协作式的调度方式减少了cache miss,,避免了无意义的切换,不仅切换次数是O(n)而且系数比较小。
但是由于协程是属于某个线程的 ,因此其实际上无法处理阻塞操作,因为其一旦进入阻塞的函数,整个线程都被阻塞住,无法在线程内部调度切换到其他的协程。因此大多数协程库的做法是遇到阻塞函数会创建一个线程并发执行阻塞函数,协程进入休眠,等待阻塞完成后通过回调唤醒该协程。

总结

进程
  1. 数据共享复杂,同步简单
  2. 进程间不会互相影响,编程简单,调试简单
  3. 适用于多核,多机分布式,拓展机器比较简单
  4. 整体执行性能上多进程并不一定比多线程慢,尤其是在linux下,线程与进程的实现方式几乎相同,唯一的区别是共享内存空间,linux的写时复制技术节省了大量的内存资源
  5. 操作系统负责切换,需要内核态,切换页表,执行栈,硬件上下文*,切换速度最慢
线程
  1. 数据共享简单,同步复杂
  2. 操作系统负责切换,需要内核态,切换执行栈,硬件上下文*,切换速度一般
协程
  1. 数据共享简单,同步简单
  2. 单凭协程实际上无法处理阻塞问题
  3. 用户负责切换,无需内核态,切换执行栈,硬件上下文*,切换速度最快

*硬件上下文:寄存器中的数据,硬件上下文切换就是指保存当前寄存器中要切换出的进程的数据,将要切换进来的进程的数据存入寄存器。

你可能感兴趣的:(操作系统)