一、前言

在go中,使用go关键字跟上一个函数,就创建了一个goroutine,每个goroutine可以认为是一个轻量级的线程,其占用更少的堆栈空间,并且其需要的堆栈空间大小可以随着程序的运行需要动态增加或者空闲回收。

二、goroutine

goroutine在go中是最小的运行单位,当我们启动了一个go程序后,运行main函数的就是一个goroutine。

package main
import (
    "fmt"
    "sync"
)
var wg sync.WaitGroup
//goroutine1
func main() {
    defer fmt.Println("----main goroutine over---")
    wg.Add(1)
    go func() { //goroutine2
        fmt.Println("Im a goroutine")
        wg.Done()
    }()
    fmt.Println("----wait sub goroutine over---")
    wg.Wait()
    fmt.Println("----sub goroutine over---")
}

如上代码我们在main函数内使用go关键字创建了一个goroutine来运行匿名函数(需要注意不要忘记添加()),创建后的这个goroutine会与main函数所在的goroutine使用相同的地址空间(类似于c中调用fork创建子线程),并发运行,而不是串行的。

另外我们也可以先创建一个函数,然后使用go关键字带上函数名就可以开启一个新goroutine来运行这个函数,如下代码创建了函数printFunc,然后使用 go printFunc()来开启新goroutine来启动该函数:

package main
import (
    "fmt"
    "sync"
)
var wg sync.WaitGroup
func printFunc() { 
    fmt.Println("Im a goroutine")
    wg.Done()
}
func main() {
    defer fmt.Println("----main goroutine over---")
    wg.Add(1)
    go printFunc()//goroutine2
    fmt.Println("----wait sub goroutine over---")
    wg.Wait() 
    fmt.Println("----sub goroutine over---")
}

另外需要注意的是在go中整个进程的生命周期是与main函数所在goroutine一致的,只要main函数所在goroutine结束了,整个进程也就是结束了,而不管是否还有其他goroutine在运行:

var wg sync.WaitGroup
func main() {
    defer fmt.Println("----main goroutine over---")
    wg.Add(1)
    go func() { //
        fmt.Println("Im a goroutine")
        wg.Done()
        //无限循环
        for{
            fmt.Println("---sub goroutine---")
        }
    }()
    fmt.Println("----wait sub goroutine over---")
    wg.Wait()
    fmt.Println("----sub goroutine over---")
}

如上代码在使用go关键字创建的goroutine内新增了for无限循环打印输出,运行上面代码后会发现随着main函数所在gorroutine销毁,后进程就退出了,虽然新创建的goroutine还没运行完毕。这点与java不同,在java中存在user用户线程与deamon线程之分,当不存在用户线程时候,jvm进程就退出了(而不管main函数所在线程是否已经结束)

goroutine是轻量级线程,并不是操作系统线程,goroutine与操作系统线程对应关系是M:N,也就是M个goroutine对应N个操作系统线程,goroutine内部实现与在多个操作系统线程(Os 线程)之间复用的协程(coroutines)一样。如果一个goroutine阻塞OS线程,例如等待输入,则该OS线程中的其他goroutine将迁移到其他OS线程,以便它们可以继续运行。

三、如何杀死一个goroutine

为了让一个运行中的goroutine停止,我们可以让其在一个channel上监听停止信号,如下代码:

package main
import (
    "fmt"
    "time"
)
func main() {
    //1
    quit := make(chan struct{})
    //2
    go func() {
        for {
            //2.1
            select {
            case <-quit: //2.1.1
                fmt.Println("sub goroutine is over")
                return
            default: //2.1.2
                //dosomthing
                time.Sleep(time.Second)
                fmt.Println("sub goroutine do somthing")
            }
        }
    }()
    //3.dosomthing
    time.Sleep(time.Second * 3)
    //4.关闭通道quit
    fmt.Println("main gorutine start stop sub goroutine")
    close(quit)
    //5
    time.Sleep(time.Second * 10)
    fmt.Println("main gorutine is over")
}
  • 如上代码1创建了一个无缓冲通道quit用来做反向通知子线程停止;代码2开启了一个goroutine,该goroutine内使用了无限循环,内部代码2.1使用了select结构,其中第一个case是从通道quit内读取元素,由于quit通道一开始没有元素,所以这个case分支不会被执行,而是转向执行defalut分支;defalut分支里面用来执行具体的业务,这里是休眠1s然后打印输出,这个goroutine的作用就是间隔1s执行打印输出,并且等quit通道内有元素时候执行return退出当前goroutine
  • 代码4关闭通道,关闭通道后回向通道内写入一个零值元素,这里代码3先让主goroutine休眠3秒是为了在关闭quit通道前让子goroutine有机会执行一些时间。
  • 代码4关闭通道后,子goroutine内的select语句的第一个case就会从quit读取操作中返回,然后子goroutine就执行return退出了。

四、总结

本节我们探讨了goroutine,可知其就是一个轻量级的线程,在go中使用关键字go就可以创建一个与调用goroutine并发运行的goroutine,使用起来很是方便,在后面章节当我们提到轻量级的线程时候,除非特殊指明,否则都是指goroutine。使用无缓冲通道和select结构在主goroutine 内反向控制子goroutine的生命周期在go中是一个通用的做法。