Go语言进阶二——Goroutine使用详解

843 阅读3分钟

我正在参加「掘金·启航计划」

概念

goroutine的概念类似于线程,是官方实现的超级“线程池”,是由go程序运行时(runtime)调度和管理。go程序会智能的将goroutine中的任务合理的分配给每个cpu

goroutine通过通信来共享内存,而不是共享内存来通信。

每个实例4~5kb的栈内存占用和由于实现机制而大幅减少的创建和销毁开销是go高并发的根本原因。

使用goroutine

在调用函数的时候在前面加 go关键字,就可以为函数创建一个goroutine

一个goroutine必定对应一个函数,可以创建多个goroutine执行同一个函数

启动单个groutine

func hello(){
	fmt.Println("goroutine hello println")
}

func main(){
		go hello() //开启一个groutine
    fmt.Println("main println")
    time.Sleep(time.Second*2)//如果不睡就会出现不输出groutine的内容,直接输出main println
}

在程序启动时,Go程序会默认给main()函数创建一个goroutine, 当main函数的goroutine结束后,所有在main函数中创建的goroutine都会一同结束。

启动多个goroutine

wg sync.WaitGroup //定义全局变量
  
func hello(){
	fmt.Println("goroutine println")
  wg.Done()//goroutine结束就登记减1
}

func main(){
	for i:=0; i<5; i++{
    wg.Add(1) //启动一个goroutine就登记加1
  	go hello()
  }
  wg.Wait()//等待所有登记的goroutine都结束
  fmt.Println("main goroutine done")
}

goroutine与线程

可增长的栈, OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2kb),goroutine的栈不是固定的,可以按需增大和缩小,限制1GB,可以一次创建十万个gogroutine

goroutine调度

GPM是go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统,区别于os系统调度os线程

  • G就是goroutine的,里面除了存放本goroutine信息外,还有与所在P的绑定等信息
  • P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的gotoutine做一些调度(比如把占用CPU时间长的gotoutine暂停,运行后续的gotoutine等),当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
  • M是Go运行时(runtime)对操作系统内核线程的虚拟,M与内核线程一般是一一映射的关系,一个goroutine最终要放到M上执行

P与M一般也是一一对应的,他们的关系是:P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G挂载在新建的M上,当旧的G阻塞完成或者认为其已经死掉时回收旧的M。

P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本后默认为屋里线程数。在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。

单从线程调度来讲,Go语言相比其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器来调度的,这个调度器使用一个成为m:n调度的技术(复用/调度m个goroutine到n个os线程)。其一大特点是gotoutine的调度是在用户态下完成的,不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池,不直接调用系统的malloc函数(除非内存池需要改变),成本比调度os线程低很多。另一方面充分利用了多核的硬件资源,把多个goroutine均分在物理线程上,再加上本身gotoutine的超轻量,保证了go调度方面的性能