# 理解进程,线程,协程

作者:戊子仲秋 (opens new window)编程导航星 (opens new window)球 编号 20000

# 为什么需要进程,线程,协程?

举个例子,比如:我们 go run main.go,启动一个 go 程序,从磁盘读取可执行文件,将 CPU 要执行的命令加载到内存中,然后 CPU 就一直:1. 读取内存指令,2. 执行指令,循环往复。那我们从磁盘读取的可执行文件,读到内存,他放在什么地方呢?

我们需要找一个结构体来记录这些东西,比如加载一个程序,第一条命令执行什么?所以我们发明了一个结构体,也就是我们的进程,操作系统分配资源的基本单位。

在多核 CPU 时代,如果我们想要充分利用核心,可以选择让不同进程跑到不同 CPU 上,但这又存在其他的问题,进程之间会带来额外的开销,比如:进程间通信。增大了编程难度的同时,也增加了系统开销。

一个进程最开始都是 main 函数开始的,可以把执行 main 函数的过程看作是一个执行流,一个进程下面有多个小任务同时执行,可以看作有多条执行流,给这个执行流起个名字,那就是线程,让这些线程去执行不同的函数(不同的任务)就好了。线程的内存地址是直接从进程里面划分出来的,这也是线程比进程创建快的一个原因,线程直接划分进程内存,而进程还需要操作系统进行分配。

对于进程和线程,他们的本质就是状态机(状态机其实就是对某种事物状态变化的一个抽象,这种抽象可以帮助我们更容易理清思路,能够明确知道,某种事物在某个时间点会有某种状态,或者经历某种操作,就切换到某种状态),而操作系统同样是一个状态机,不过他是来调度线程/进程的,可以叫做调度状态机的状态机。

Linux 的 二把手 alan cox 说过一句话:计算机就是状态机,线程是为不懂状态机的程序员准备的 [手动狗头]

其实线程和进程都是对底层的一种高度抽象,线程同样,线程这个概念甚至在 Linux 里面他都不算存在。进程在 Linux 里面是一个 task_struct,线程同样是,只不过线程是共享了进程的空间,可以看作创建了一个线程,其实就是创建了一个新进程,一个可以共享原来进程内存空间的的进程,我们把它抽象的叫做线程。

线程有两种组织形式,简单来说:

1)用户级线程

我们将操作系统分为两态,用户态和内核态,用户态就是用户可以看到的空间,而用户级线程就是从用户视角可以看到的线程,而用户级的线程在切换的时候在用户空间就能完成,不需要切换到核心态,线程切换的开销比较小,效率高。

2)内核级线程

线程调度、切换等工作都由操作系统内核负责。

在支持内核级线程的系统中,根据用户级线程和内核级线程的映射关系,可以划分为几种多线程模型:

一对一:一个内核级线程对一个用户级线程

一对多:一个内核级线程对多个用户级线程

多对多:多个内核级线程对多个用户级线程

各有优缺点,具体是什么策略主要看使用的系统调用,这里我就不详细展开了,可以看看这个一对多模式的图:

在用户程序中,比如线程库,组织实现的一个执行流,他也起个别名叫做线程,因为内存在用户空间,所以叫用户线程合适一些,可以看作是一个 C语言程序自己实现了一套线程组织方式,但实际上,最终调度还是要通过内核线程,举个例子:像 pthread 这样的线程库,他可以创建n个用户级线程,但其实底层就是一个线程负责

用户级线程也是一个抽象的状态机,只不过是在用户空间抽象的,而不是内核空间,那为什么要抽象一个用户级线程呢?因为:如果都用内核级线程的话,线程切换每次都要在用户态和内核态之间来回走,系统开销大

用户级线程优点是:

  1. 管理开销小:创建、销毁不需要系统调用,也不占用内核内存
  2. 切换成本低:用户空间程序可以自己维护,不需要通过操作系统调度

但他也有缺点:

  1. 用户程序在管理,当它进行 I/O 的时候,无法利用到内核的优势,需要频繁进行用户态到内核态的切换
  2. 无法利用多核优势:比如操作系统调度的仍然是这个线程所属的进程,所以无论每次一个进程有多少用户态的线程,都只能并发执行一个线程,无法实现并行,因此无法利用多核 CPU 的优势
  3. 操作系统无法优化用户线程的调度,因为对于操作系统,用户线程是完全透明的(感知不到),如果用户线程阻塞了,操作系统也无法快速切换阻塞的用户线程

但 There is no silver bullet!没有最好的解法,只有最适合的业务场景。

那为什么需要协程呢?

我们都知道,操作系统基本的都是分时系统,也就是给每一个进程/线程都分配时间片,时间片用完了,就打断该线程的执行,让其他线程享受 CPU,这会带来并发安全的问题(我们可以通过加锁解决),但有些线程其实是可以相互配合的,如果在一个线程还没有执行完就抢占式的把 CPU 资源分配给其他线程,很可能导致意想不到的执行紊乱情况

操作系统的分时机制的好处是让每个线程都能享受到 CPU 资源,但有些场景也是坏处,比如我同一个进程下的多个线程,其实我可以让他们相互配合,协作的,但由于操作系统的分时抢占式,就容易打破协作的平衡

“非抢占式多任务”,当出现一个线程事情没做完,但还差一点点的时候,其他线程就先等一会儿,等做完了就自动交出 CPU 控制权,这里体现出的协作,我们给这种形式的执行流起一个名字,那就是所谓的协程,每次执行中,协程之间的具体执行顺序可能千变万化,但协程执行权切换却只会发生在用户明确放弃执行权之后:比如你明确执行了 yield 语句时(让出 CPU 的语句)

但是,一般很难开发出一套好用的协程库,所以现在用的最多的还是线程。

# 进程,线程,协程有什么不同?

# 进程与线程有什么不同?

1)从定义上说,进程是操作系统进行资源分配和调度的基本单位,而线程是进程的执行单元,共享进程的资源

2)具体来说,进程就是运行起来的可执行程序,当我们运行一个可执行程序的时候,就会创建一个或多个进程,创建进程的时候需要分配空间,比如:栈区、文件映射区、堆区、静态区、常量区、代码段,这就是为什么进程也被称为是资源分配的基本单位;每个进程中都有唯一的主线程,有且只有一个,主线程和进程是相互依存的关系,主线程结束进程也会结束

3)每个进程有自己的独立地址空间,不与其他进程分享;一个进程里可以有多个线程,彼此共享同一个地址空间。堆内存、文件、套接字等资源都归进程管理,同一个进程里的多个线程可以共享使用。每个进程占用的内存和其他资源,会在进程退出或被杀死时返回给操作系统

4)并发应用开发可以用多进程或多线程的方式,多线程由于可以共享资源,效率较高;反之,多进程默认是不共享地址空间与资源,开发较为麻烦,共享数据时效率较低。但多进程安全性较好,在某一个进程出问题时,其他进程一般不受影响,而在多线程的情况下,一个线程执行了非法操作会可能导致整个进程退出

# 协程和线程有什么区别?

1)协程的开销比线程更小

2)协程可以很好的应对非抢占式多任务场景,大家一起办好一件事;而协程在操作系统的分时机制下,容易出现导致意想不到的执行紊乱情况

3)线程可以实现并行,但协程不行,他与用户级线程有些类似,所以他也拥有用户级线程的优点:创建、销毁不需要系统调用,也不占用内核内存,开销小;在用户空间程序可以自己维护协程,不需要通过操作系统调度

这里要注意的是,go 的协程 goroutine 其实不算操作系统的协程,他是 go 自己实现的便于并发编程的东西,这个的东西又有很多可以说道的地方,不过这里就不展开了

最近更新: 12/26/2023, 1:17:29 PM
编程导航   |