Go语言goroutine
在别的语言里想要在一个程序中实现多任务,如python,python实现多任务可以使用多进程、多线程、携程。但多进程占用资源,多线程无法发挥多核的优势(GIL),python的协程是单线程的,必须等一个任务作出让步,另一个任务才能执行,如果其中一个任务阻塞住,让不出cpu来,那么整个程序都会被阻塞住。
go语言的goroutine(协程)是一个类似于线程的概念,但是它比线程轻量。当开启了多个goroutine后,程序会将每个goroutine分配给每个cpu的核心,可以充分发挥cpu多核的效率。go的协程效率比为m(cpu核数)/n(goroutine数),而python的协程的效率比为1/n
开启goroutine
只需要在将任务封装成一个函数,使用go关键字,就可以开启一个goroutine
完成多任务。同时,程序的主函数也是一个goroutine
package main
import "fmt"
func work() {
fmt.Printf("work goroutine")
}
func main() {
go work()
fmt.Println("main goroutine")
}
sync.WaitGroup
上面的程序启动goroutine是没问题,但是编译运行后会发现只有打印了main goroutine
。这是因为,开启在主goroutine里面启动另外一个goroutine后,另外开启的goroutine还没来得及运行,主goroutine就已经结束了,主goroutine结束,由主goroutine中开启的goroutine全部都会结束,所有主goroutine想要其他的goroutine执行,必须得等待。
package main
import (
"fmt"
"time"
)
func work() {
fmt.Println("work")
}
func main() {
go work()
fmt.Println("main goroutine")
time.Sleep(time.Second)
}
使用time.Sleep来是程序休眠,来达到让另外goroutine由充足的时间来运行
但是当开启了多个goroutine后,我们不知道全部的协程运行完毕需要多少时间,我们就无法估量sleep的时间。sleep的时间可能多了,可能少了,少了就会让能成达不到想要的目的,多了就会让程序多休眠,浪费资源,此时,就需要引入sync模块的工具来解决这个问题
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func work() {
defer wg.Done()
fmt.Println("work")
}
func main() {
wg.Add(1)
go work()
fmt.Println("main goroutine")
wg.Wait() // 等待所有的协程能全部运行完毕
}
使用sync.WaitGroup
,当需要开启一个协程前,往WaitGroup
里面加1,在协程结束后,向WaitGroup
告知已经完成(可以配合defer 关剪字使用),最后在主goroutine(main函数)的最后使用WaitGroup
来等待所有的goroutine运行结束(对应的函数执行结束)
goroutine和线程的关系
这里的线程指的是操作系统的线程,线程和goroutine启动占用的资源不一样,线程的栈内存一般为2mb,而goroutine在开始的时候占用的栈内存为2kb,goroutine的栈不是固定的,它可以像go的切片一样扩容,最大限制可以达到一个G,所有在go里面可以很容易开启十万个goroutine。
- GMP调度
- G:G代表goroutine,里面存放当前的goroutine的信息和当前goroutine所载的P的绑定等信息
- M:M是go运行时对操作系统的线程的虚拟。一个goroutine最终都是要放在M上运行的
- P:P管理者一组goroutine队列,P里面存放着当前的goroutine的上下文,P会对当前管理的goroutine队列做一些调度(某些goroutine可能占用cpu时间过长,P会将这个goroutine暂停,然后去让别的goroutine来执行)。当自己的goroutine全部运行结束了,这个P会去全局的队列里面取,如果全局的里面也没有了,它甚至会去别的P里面取获取goroutine,这样可以实现效率的最大化
- P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。