go 并发编程
go 并发编程
1.1 并发模型
任何语言的并行,到操作系统层面,都是内核线程的并行。同一个进程内的多个线程共享系统资源,进程的创建、销毁、切换比线程大很多。从进程到线程再到协程, 其实是一个不断共享, 不断减少切换成本的过程。
python模型
c++进程,java进程 goland并发进程
| 协程 | 线程 |
|---|---|
| 创建数量 | 轻松创建上百万个协程而不会导致系统资源衰竭 |
| 内存占用 | 初始分配4k堆栈,随着程序的执行自动增长删除 |
| 切换成本 | 协程切换只需保存三个寄存器,耗时约200纳秒 |
| 调度方式 | 非抢占式,由Go runtime主动交出控制权(对于开发者而言是抢占式) |
| 创建销毁 | goroutine因为是由Go runtime负责管理的,创建和销毁的消耗非常小,是用户级的 |
查看逻辑cpu核数
fmt.Println(runtime.NumCPU())
如果是8核,那么cpu最多跑到800%,如果是2核,最多200%
2.1 MPG并发模型
- M(Machine)对应一个内核线程。
- P(Processor)虚拟处理器,代表M所需的上下文环境,是处理用户级代码逻辑的处理器。P的数量由环境变量中的GOMAXPROCS决定,默认情况下就是核数。
- G(Goroutine)本质上是轻量级的线程,
- G0正在执行,其他G在等待。M和内核线程的对应关系是确定的。G0阻塞(如系统调用)时,P与G0、M0解绑,P被挂到其他M上,然后继续执行G队列。G0解除阻塞后,如果有空闲的P,就绑定M0并执行G0;否则G0进入全局可运行队列(runqueue)。P会周期性扫描全局runqueue,使上面的G得到执行;如果全局runqueue为空,就从其他P的等待队列里偷一半G过来。
下图对应上面最后一个
如上图,所以即使G0原先是排行第一个执行的,但是由于G0中间的系统代码调用较慢,其他G等不了G0执行完,跟着P到M1了,而G0执行完后没有p,后续非系统代码无法运行,非系统代码必须由P虚拟处理器来执行,所以G0会进入队列,等待M下空闲的P来认领,所以他们的执行顺序是不固定的,并且内核线程M和用户G的线程比例是m:n
内核线程和M之间是虚线,不可打断的
3.1 Goroutine的使用
线程都是并行的,启动协程的两种常见方式:
func Add(a, b int) int {
fmt.Println("Add")
return a + b
}
go Add(2, 4)
go func(a, b int) int {
fmt.Println("add")
return a + b
}(2, 4)
优雅地等子协程结束:
wg := sync.WaitGroup{}
wg.Add(10) //加10
for i := 0; i < 10; i++ {
go func(a, b int) { //开N个子协程
defer wg.Done() //减1
//do something
}(i, i+1)
}
wg.Wait() //等待减为0
如下为例
- 父协程结束后,子协程并不会结束。
- main协程结束后,所有协程都会结束。
- 子协程退出后,孙协程如果还没跑完不会退出,会根据main来跑
func main() {
go Ad() //Ad里面有sleep 100毫秒,如果不加下面的sleep 150毫秒,main就直接退出了,而协程还没跑完也跟着退出了
time.Sleep(150 * time.Millisecond) //如果去掉这个,就不会打印over
}
func Ad(){
time.Sleep(100 * time.Millisecond)
fmt.Println("over")
}
使用wg
var wt = sync.WaitGroup{}
func main() {
wt.Add(2) //次数加2
go Ad() //运行两个协程
go Ad()
wt.Wait() //等待减到0,如果没有wt.Done,会报错deadlock,而不会因为没有wt.Done而一直傻傻的等
}
func Ad(){
time.Sleep(100 * time.Millisecond)
fmt.Println("over")
wt.Done() //每次减1,刚好两次减到0,如果没有减到0也会报错deadlock
}
例子2
类似代码但是打印结果是不同的
func main() {
//gor1()
gor2()
}
func gor1() {
num := []int{1,2,3,4}
for _,ele := range num {
go func() { //虽然是for循环的列表,开了4个协程,当子协程执行print的时候,ele已经变成4了
fmt.Println(ele) //打印的可能是 4 4 4 4
}()
}
time.Sleep(100 * time.Millisecond)
}
func gor2(){
num := []int{1,2,3,4}
for _,ele := range num {
go func(v int) {
fmt.Println(v) //打印的可能是 4 1 2 3
}(ele) //每次拿到的参数都是不同的
}
time.Sleep(100 * time.Millisecond)
}