「深入学习 Golang」之 Goroutine
进程、线程、协程
进程
- 进程是系统资源分配的最小单位
- 进程包括
text region
,data region
和stack region
等 - 进程的创建和销毁都是系统资源级别的,因此是一种比较昂贵的操作
- 进程是抢占式调度,他有三个状态:等待态、就绪态、运行态
- 进程之间是相互隔离的,每个进程有各自的系统资源,更加安全同时也带来了进程间通信不便的问题
- 进程是线程的载体
线程
- 同一个进程的多个线程共享进程的资源
- 每个线程也拥有自己的一少部分独立的资源
- 线程相比进程更加轻量,同一个进程内的多个线程通信比多个进程间通信容易,同时也带来了同步、互斥和线程安全的问题
协程
- 协程调度不需要内核参与,是完全由用户态程序决定的
- 协程不是抢占式调度,多个协程进行协作调度,避免了系统切换开销导致 CPU 使用率高
goroutine
协程并不是 Go 语言的特有机制,Go 语言是在原生语言层面支持的的协程。Go 语言可以通过通信实现 goroutine 之间的数据共享。
Golang 中,不要通过共享内存来通信,而应该通过通信来共享内存!
- 创建一个 goroutine 不需要太多的内存,大概 2KB 左右栈空间
- goroutine 的创建、调度和销毁是
runtime
完成的,runtime
会被分配一些线程,用来运行所有的 goroutine 在任何时候每个线程都只会运行一个 goroutine,如果一个 goroutine 被阻塞会进入休眠状态,待操作完成后再恢复,另外一个 goroutine 会替换它到对应的线程上运行 - goroutine 的调度是
协作式
的。当切换 goroutine 时,调度器只需要保存和恢复三个寄存器:程序指针、栈指针和DX,切换开销很小 - goroutine 的退出机制是,goroutine 的退出只能由本身控制,不允许外部强制结束该 goroutine。两种例外:1.main函数结束,2.程序崩溃结束运行。要实现主 goroutine 控制子 goroutine 的开始和结束,需要借助其他方式,比如context、channel、全局变量
goroutine 中的三个实体
- G
- 代表一个 goroutine 对象,每次使用
go
调用的时候都会创建一个 G 对象,它包括栈
,指令指针
和调用 goroutine 重要的其他信息,比如阻塞它的 channel
- 代表一个 goroutine 对象,每次使用
- M
- 代表一个线程,每次创建一个M的时候,都会用一个底层的线程被创建。所有的G任务最终还是在M上执行
- M会从绑定的运行队列(P)中取出G,然后运行G,如果G运行完毕或进入休眠状态,则从运行队列中取出下一个G,周而复始
- P
- 代表一个处理器,每一个运行的M都必须绑定一个P,就像线程必须在每一个cpu核上执行一样。P会调度G到M上运行。P的个数就是
GOMAXPROCS
(最大256),M的个数和P不一定一样多,每个P有自己单独的本地G任务队列,也会有一个全局的G任务队列 - P可以理解为控制Go代码并行度的机制
- 代表一个处理器,每一个运行的M都必须绑定一个P,就像线程必须在每一个cpu核上执行一样。P会调度G到M上运行。P的个数就是
相比较大多数并行设计模式,Golang 比较有优势的设计就是P上下文这个概念的出现,如果只有G和M的对应关系,那么当G阻塞在I/O上的时候,M是没有实际工作的,这样造成了资源的浪费,没有了P那么所有的G的列表都放在了全局,会导致临界区太大,对多核调度造成极大影响
调度器模型 GPM
相关 GPM 的内容参考:https://blog.linganmin.cn/posts/golang-gpm.html
并发控制
并行和并发
- 并发
- 指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个任务同时执行的效果,但在微观上并不是同时执行的,只是把时间分成了若干段,使得多个进程快速交替执行。
- 并行
- 指在同一时刻有多条指令在多个处理器上同时运行。
并发编程在 CPU 密集型的程序中无法提升程序性能,更适用在在 I/O 密集型的程序中。
并发实现模型
- 多进程
- 优点
- 每个进程独立,都有自己单独的内存空间和上下文信息。
- 缺点
- 开销大
- 进程间通信难
- 进程间内存共享不方便
- 优点
- 多线程
- 优点
- 开销小
- 轻量,创建和销毁成本低
- 线程之间通信和共享内存方便
- 缺点
- 频繁创建和销毁会导致开销大,可使用线程池
- 数据紊乱、死锁
- 优点
- 协程
- 优点
- 不基于操作系统,基于程序,开销小
- 优点
Golang 中的 CSP 并发模型
我们常见的多线程模型一般是通过共享内存实现的,但共享内存就会出现如:资源抢占、一致性的问题。为了解决这些问题,需要引入多线程锁、原子操作等限制来保证程序执行结果的正确性。
除了共享内存模型外,还有一个经典的模型就是 CSP 模型。CSP 全拼是 Communicating Sequential Processes,大致意思是 通信顺序进程。CSP 描述了并发系统中的交互模式,是一种面向并发的的语言的源头。
Golang 只使用了CSP中的Process/Channel
的部分,简单的说就是Process
映射=> Goroutine
,Channel
映射=> Channel
。Goroutine 之间是没有任何耦合,可以完全并发执行,Channel 用于给 Goroutine 传递消息,保持数据同步。
控制并发的方法
并发数据安全
- 锁
- sync.Mutex
- 互斥锁
- sync.RWMutex
- 读写锁
- sync.Mutex
- 原子操作
- sync
- atomic
并发行为控制
开发 go 程序时,经常要使用 goroutine 并发处理任务,有时候这些 goroutine 是相互独立的,有时候多个 goroutine 之间往往是需要同步数据通信的,还有一种情况是,主 goroutine 需要控制所属于它的所有子 goroutine 。
实现 goroutine 间通信的方式大致如下
- 全局变量
- 实现
- 声明一个全局变量
- 所有子 goroutine 共享该全局变量,并不断轮训检测变量是否有更新
- 主 goroutine 更新该变量
- 子 goroutine 检测到全局变量更新,执行相应逻辑
- 实现
- Channel 通信
- 场景
- 某个任务中的某一个或多个 goroutine 依赖另一个 goroutine 中的条件或产生的结果
- 实现
- channel + select
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
37package main
import (
"fmt"
"time"
)
func main() {
finish := make(chan bool)
go func() {
for true {
fmt.Println("hello 小下")
// 监听通知
select {
case <-finish:
return
default:
time.Sleep(time.Millisecond * 500)
}
}
}()
go func() {
// 使用 sleep 模拟耗时
time.Sleep(time.Second * 2)
// 通知完成
finish <- true
}()
// 防止 main 过早退出
time.Sleep(time.Second * 5)
}
- channel + select
- 场景
- Context
- 场景
- 多层级的 goroutine 之间的信号传递
- 实现
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
52package main
import (
"context"
"fmt"
"time"
)
func foo(ctx context.Context) {
go bar(ctx)
for true {
fmt.Println("hello 小下")
select {
case <-ctx.Done():
fmt.Println("foo exit.....")
return
default:
time.Sleep(time.Millisecond * 400)
}
}
}
func bar(ctx context.Context) {
for true {
fmt.Println("hello world")
select {
case <-ctx.Done():
fmt.Println("bar exit.....")
return
default:
time.Sleep(time.Millisecond * 600)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go foo(ctx)
// 防止过早发送退出信号
time.Sleep(time.Second * 2)
cancel()
// 防止过早退出
time.Sleep(time.Second * 5)
}
- 场景