进程、线程和协程
这个问题太常提到了,说难不难,但往深了说又有很多可挖掘的细节。当前仅记录这三者的概念、区别和关系,更精准的差异需要参考相关书籍。
进程
进程是操作系统对一个正在运行的程序的一种抽象,进程是资源分配的最小单位。系统是给进程直接分配资源,它在运行过程中持有相关上下文(寄存器)。
进程存在的意义是:CPU速度太快,而单个任务(job)不一定实时的使用CPU,可能会等待其他IO,此时对于CPU就是一种浪费,所以形成了一个新的使用场景。多个任务同时运行,按照不同的CPU调度策略进行分配,在计算机科学中,单个的任务就叫做进程。
一般进程有三种状态:
- 运行态:正在调用CPU
- 就绪态:啥都好了,就差CPU空出来了
- 阻塞态:需要等待其他事件(例如文件IO,网络IO)
需要注意的是,现在常用的CPU调度模型为:时间片轮转法,它主要适用于多个进程按照时间片进行轮流调用,在用户看来,这台电脑就像是同时运行多个进程,实际上单个 CPU 只能运行单个进程,只是切换的速度太快了,用户无感知。
线程
进程是一个缩写,代表:进行的程序。所以对于大部分程序来说,都是只启动一个进程的,也可以说一个进程就是一个实例。它的粒度是比较大的,程序内部的代码是有多个分支或者程序段的,这时就引出了线程概念。线程是依托于进程的,它们之间可以共享进程所持有的上下文信息,对于关键资源还会通过锁的方式进行线程间的限制。线程运行时的本质和进程一样,都是要占据 CPU 一段时间进行运算。
进程和线程的关系
进程是资源分配的最小单位,线程是 CPU 调度的最小单位。
做个简单的比喻:进程=火车,线程=车厢
- 线程在进程下行进(单纯的车厢无法运行)
- 一个进程可以包含多个线程(一辆火车可以有多个车厢)
- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
- 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
- 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-”互斥锁”
- 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
协程
协程其实不算是操作系统的概念,不属于内核层,它更多的是编程层面的概念,属于用户态(即协程是由编程语言实现,由开发者自己创建并调用)。
协程与进程、线程相比并不是一个维度的概念,协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。
线程的栈有 8 MB,而协程栈的大小通常只有 KB,而 Go 语言的协程更夸张,只有 2-4KB,非常的轻巧。
优点:
- 内存占用小:协程更加轻量,创建成本更小,降低了内存消耗,协程一般只占据极小的内存(2~5KB),而线程是 1MB 左右。虽然线程和协程都是独有栈,但是线程栈是固定的,比如在Java中,基本是 2M,假如一个栈只有一个打印方法,还要为此开辟一个 2M 的栈,就太浪费了。而 Go 的的协程具备动态收缩功能,初始化为 2KB,最大可达 1GB
- 节省 CPU:避免系统内核级的线程频繁切换,造成的 CPU 资源浪费。好钢用在刀刃上。而协程是用户态的线程,用户可以自行控制协程的创建于销毁,极大程度避免了系统级线程上下文切换造成的资源浪费。
- 减少了同步锁:协程最终还是运行在线程上,本质上还是单线程运行,没有临界区域的话自然不需要锁的机制。多协程自然没有竞争关系。但是,如果存在临界区域,依然需要使用锁,协程可以减少以往必须使用锁的场景
- 稳定性:前面提到线程之间通过内存来共享数据,这也导致了一个问题,任何一个线程出错时,进程中的所有线程都会跟着一起崩溃。
- 异步逻辑清晰:使用协程在开发程序之中,可以很方便的将一些耗时的IO操作异步化,例如写文件、耗时 IO 请求等。
缺点:
- 无法利用多核资源:协程运行在线程上,单线程应用无法很好的利用多核,只能以多进程方式启动。
- 协程不能有阻塞操作:线程是抢占式,线程在遇见IO操作时候,线程从运行态→阻塞态,释放cpu使用权。这是由操作系统调度。协程是非抢占式,如果遇见IO操作时候,协程是主动释放执行权限的,如果无法主动释放,程序将阻塞,无法往下执行,随之而来是整个线程被阻塞。
- CPU密集型不是长处:假设这个线程中有一个协程是 CPU 密集型的他没有 IO 操作,也就是自己不会主动触发调度器调度的过程,那么就会出现其他协程得不到执行的情况,所以这种情况下需要程序员自己避免。