goroutine

143 阅读3分钟

这是我参与青训营的第4篇笔记。

什么是goroutine

在java或者C++中我们要实现并发编程时通常需要维护线程池去调度线程执行任务。Go语言已经将这种并发编程封装得很好,Go语言中的goroutine就是一种封装性良好的技术。goroutine的概念类似于线程,但是goroutine是由Go的运行时调度和管理的。Go程序会智能地将goroutine中的任务合理分配给每个CPU。

因此在Go语言中你不需要自己去写进程、线程、协程等,你只需要goroutine去帮你执行任务即可

使用goroutine

启动单个goroutine

使用时只需要在函数前加上go关键字即可

func hello(){
    fmt.Println("asdf")
}

func main() {
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
}

这里需要注意的是当main函数返回时goroutine就结束了,所有在main函数中启动的goroutine都会结束

启动多个goroutine

var wg sync.WaitGroup

func hello(i int) {
    defer wg.Done() // goroutine结束就登记-1
    fmt.Println("Hello Goroutine!", i)
}
func main() {

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

goroutine与线程

可增长的栈

OS线程一般都有固定的栈内存,一个goroutine的栈内z存在其生命周期开始时只有很小的内存,goroutine的栈不是固定的,它可以根据需求增大和缩小,goroutine栈代销限制可以达到1GB,所以在GO语言中一次性可以创建十万个左右的goroutine

goroutine调度

GPM是GO语言运行时层面的实现,是Go语言自己实现的一套调度系统,区别于OS调度:

  • G很好理解,就是个goroutine的数据结构,里面除了存放本goroutine的信息外还有与所在P的绑定等信息
  • P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针、堆栈地址、地址边界),P会对自己管理的goroutine队列做一些调度,当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了回会去其他的P的队列里抢任务
  • M是Go运行时对OS内核线程的虚拟,M与内核线程一般是一一映射的关系,goroutine最终是要放到M上执行的

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

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

单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时自己的调度器调度的,这个调度器使用一个称为m:n调度的技术。其一大特点就是goroutine调度是在用户态下完成的,不涉及内核态和用户态的频繁却换,包括内存的分配和释放,都是用户态维护着一块大的内存池,不直接调用系统的malloc函数,成本比调度OS线程低很多

runtime包

runtime.Gosched()

让出CPU时间片,重新等待安排任务

package main

import (
    "fmt"
    "runtime"
)

func main() {
    go func(s string) {
        for i := 0; i < 2; i++ {
            fmt.Println(s)
        }
    }("world")
    // 主协程
    for i := 0; i < 2; i++ {
        // 切一下,再次分配任务
        runtime.Gosched()
        fmt.Println("hello")
    }
}

runtime.Goexit()

退出当前协程

package main

import (
    "fmt"
    "runtime"
)

func main() {
    go func() {
        defer fmt.Println("A.defer")
        func() {
            defer fmt.Println("B.defer")
            // 结束协程
            runtime.Goexit()
            defer fmt.Println("C.defer")
            fmt.Println("B")
        }()
        fmt.Println("A")
    }()
    for {
    }
}