概念
进程
- 操作系统为了执行程序,给他分配文本域、数据区域和堆栈,让程序活动起来,活动的程序可以叫做进程。
- 文本区域存储处理器执行的代码。
- 数据区域存储变量和进程执行期间使用的动态分配的内存。
- 堆栈区域存储着活动过程调用的指令和本地变量。
线程
- 线程是操作系统能够进行运算调度的最小单位,是进程中的实际运作单位。
- 一个进程中可以并发多个线程,每条线程并行执行不同的任务。
- 同一进程中的多条线程将共享该进程中的全部系统资源,但同一进程中的多个线程有各自的调用栈,自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
- 线程间通信主要通过共享内存,上下文切换很快,资源开销较少。
协程
- 协程不是由操作系统管理,而是完全由程序控制(代码怎么写的)
- 一个线程可以包含多个协程,但这些协程一定是串行的。
- 一个协程实际上是一个函数,这个函数可以在某个地方挂起,并且可以重新在挂起处外继续运行。
- 协程切换是由代码控制的,不会陷入内核态,所以效率高。
进程与线程关系
- 一个进程至少包含一个线程,如果只包含一个线程,那么所有代码只能串行。
- 每个进程的第一个线程会随着该进程的启动而被创建,可以被称为其所属进程的主线程。
- 如果一个进程包含了多个线程,其中的代码就可以被并发的执行。除了进程的第一个线程之外,其他的线程都是由进程中已存在的线程创建的。
即主线程之外的其他线程只能由代码显式地创建和销毁。
Go语言中的场景
- 在Go程序,Go的运行时系统会帮助我们自动地创建和销毁系统级线程(可以理解为Go已经帮你把创建和销毁线程的代码写了)
- 用户级线程指的是架设系统级线程之上的,由编写的程序完全控制的代码执行流程,用户级线程的创建、销毁、调度完全需要我们自己实现。(协程)
- Go语言不但有着独特的并发编程模型(通过通信来控制),以及用户级线程goroutine,还拥有强大的用于调度goroutine,对接系统级线程的调度器。
调度器
- Go语言调度器是运行时系统的重要组成部分,主要负责管理并发编程模型中的三个主要元素:G(goroutine)、P(processor)和M(machine)。
其中的M指代的就是系统级线程,P指的是能够承载多个G,使G适时地与M对接,并得到真正运行的中介
- 由于P的存在,G和M是多对多的关系。当一个正在与某个M对接并运行着的G,需要暂停(比如等待I/O或锁的解除),调度器会把G和M分开,让排队的其他G上。
- 当一个G需要恢复运行,调度器会给他找空闲的计算资源(包括M),M不够用了,调度器会向操作系统申请新的M。当某个M没用了,调度器又会申请销毁他。
goroutine
- Go语言的入口是main函数,主goroutine的函数就是这个main函数。
- go函数(go关键字后面的函数)真正执行时间,总会和其所属go func语句被执行时间不同。即声明完了不会立马执行。
- 程序执行到go func语句,Go运行时系统会先试图从空闲G的队列中拿一个(复用),如果没有空闲的,就去创建一个(创建成本很低,只需要上下文环境)。
- 拿到G后,运行时系统用这个G去包装func中的代码,再把这个G追加到存放可运行G的队列中,由调度器安排。
- 主goroutine中的代码执行完了,Go程序结束,其他goroutine再也不会运行。
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
go func() {
fmt.Println(i)
}()
}
}
- 上面这个代码,绝大多数情况什么都不打印,即主goroutine结束,其他goroutine未运行。
- 如果在末尾加上等待,打印出的i不再是声明时的i,即有可能是10个10,也有可能有其他数。
- 如果将i作为参数传到func中,则会打印0-9,但是顺序无法保证。