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)
}

4.1

Go