什么是协程

介绍协程之前,首先回顾一下进程与线程

进程与线程

进程: 通俗点讲,进程就是一个程序运行的实例,进程拥有自己运行时打开的各种资源和独立的内存空间。每个进程通过PCB(process control block)来保存自己的基本信息(在Linux中,该结构叫task_struct)
PCB包含了进程运行的基本信息:标示符、状态、优先级、程序计数器、内存指针、上下文、IO信息、记账信息等

线程: 线程属于进程,是程序的实际执行者,一个程序可以包含多个线程,线程具有自己的栈空间,其基本信息存储在TCB(thread control block)

对OS来说,进程是最小的资源管理单元,线程是最小的执行单元

线程切换比进程切换更加轻量:线程进程切换时,都会调用内核切换上下文,不同进程之间内存空间相互独立,而不同线程之间共享进程的内存空间,因此线程上下文切换效率更高

进程和线程的切换都是抢占式的,由OS管理

go语言协程

协程,是一种比线程更轻量级的运行单元。
主要特点:

  1. 轻量级“线程”
  2. 非抢占式多任务处理,由协程调度器来调度任务也可主动交出控制权
  3. 编译器、解释器、虚拟机层面的多任务
  4. 多个协程可以在一个或多个线程上运行

非抢占式测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import (
"testing"
"fmt"
"time"
)

// 结束时间大于2秒,因为是非抢占式,main的协程无法及时得到执行
// 打印属于IO操作,执行打印时会释放CPU资源给其他协程使用
func Test_01(t *testing.T) {

for i := 0; i < 100; i++ {
go func(x int) {
for {
fmt.Println(x)
}
}(i)
}
time.Sleep(time.Second * 2)
}
// 该程序不会退出,执行到最后一行打印之前,启动了4个协程,这些协程均不会释放CPU资源,没有空闲的线程用于主协程执行,因此不会退出
// 当启动的协程数 < runtime.NumCPU() ,可以正常退出
func Test_02(t *testing.T) {

// N值要大于等于机器支持线程数(大于等于runtime.NumCPU())
// 测试机为2核4线程,这里设置成4
const N = 4
var a [N]int
for i := 0; i < N; i++ {
go func(x int) {
for {
a[x]++
}
}(i)
}
// 此处休眠1秒是为了让前边循环中的协程都启动
time.Sleep(time.Second)
fmt.Println(a)

}
// 主动交出控制权
func Test_03(t *testing.T) {

//
const N = 10
var a [N]int
for i := 0; i < N; i++ {
go func(x int) {
for {
a[x]++
// 主动交出控制权
runtime.Gosched()
}
}(i)
}
time.Sleep(time.Second)
fmt.Println(a)

}
  1. 任何函数只要加上go就可以交给调度器执行
  2. 调度器会在指定的点进行切换
    可能切换点:(可能切换)
    1. IO,select
    2. channel
    3. 等待锁
    4. 函数调用(当协程运行过长时会打标记,在函数调用时检测标记并根据标记决定是否调度)
    5. runtime.Gosched()
    6. 系统调用
  3. 可以通过使用 -race参数来进行数据多线程的读写竞争进行检测

go语言协程模型

模型简介

如下图,go语言协程的模型
go协程模型

  1. M:系统线程(内核线程)
  2. P:执行Go协程所必须的资源(上下文环境,逻辑处理器)
  3. G:Go协程

可以把Go协程视为“用户线程”,它由用户态的Go调度器来实现在P上的调度,由于P中的协程队列同处于一个上下文环境(在同一个线程)没有内核切换的开销。调度器通过将Go协程映射到内核线程中,使程序运行时“看起来”是在并发执行。

由于Go协程是非抢占式的,而且基本在指定的几种情况下才会释放CPU,所以在协程之间切换时程序上下文复制开销较小且不用进行内核调用,因此更轻量级。

模型

在并发执行程序时,调度器除了正常从协程队列里取出协程放到自己的执行队里中外,还有其他的执行调度行为:

  1. Work-sharing:当处理器生成新线程时,它会尝试着将一些工作任务交给其他闲置的线程执行,充分利用资源
  2. Work-stealing:调度器主动“偷取”其他线程上执行任务来执行

在Go语言中有一个global goroutine queue,每个内核线程又有一个local goroutine queue
对于每一个内核线程来说,在该线程上等待执行的协程是串行的,而对于外部来说,这些协程是并发的。

协程之于内核线程,有点像进程之于CPU(单核CPU上对于CPU来说进程是串行的,对于外部来说是并行的)