goroutine

Go语言的并发,就不得不提goroutine,其作为Go语言的一大特色,在日常开发中使用很多

Go goroutine理解

这里就不对goroutine进行描述了,直接进入正文

go并发控制方法主要有:

  1. 公共变量
  2. channel
  3. WaitGroup
  4. context

公共变量

这是并发控制最简单的实现方式

  1. 声明一个公共变量
  2. 所有子goroutine共享这个变量,并不断轮询这个变量并检查是否有更新
  3. 在主进程中变更该公共变量
  4. 子goroutine检测到公共变量更新,执行相应的逻辑
package main

import (
   "fmt"
   "time"
)

func main() {
    // 公共变量
    state := true

    go func(){
        for state {
            println("goroutine A running")
            time.Sleep(1 * time.Second)
        }
        println("goroutine A exit")
    }()

    go func() {
        for state {
            println("goroutine B running")
            time.Sleep(1 * time.Second)
        }
        println("goroutine B exit")
    }()

    time.Sleep(2 * time.Second)
    state = false
    time.Sleep(2 * time.Second)

    fmt.Println("main func exit")
}

输出结果

goroutine B running
goroutine A running
goroutine A running
goroutine B running
goroutine B running
goroutine A running
goroutine A exit
goroutine B exit
main func exit

这种实现方式比较简单,但如果逻辑过于复杂,则变量不容易控制

channel

channel是goroutine之间主要的通讯方式,一般会和select搭配使用

Go channel 实现原理分析

channel在这里也不做过多描述,大致处理过程为:

  1. 声明一个可以判断结束的chan
  2. 在goroutine中,使用select判断chan接收到的值。如果接收到结束,就可以走结束逻辑。如果没有,那么就会执行预设的逻辑,或者default
  3. 主程序发送了结束的指令后
  4. 子goroutine接到结束指令,进行结束逻辑
package main

import (
   "fmt"
   "time"
)

func main() {
    // 初始化一个接收bool的channel
    c := make(chan bool)

    go func() {
        for {
            select {
            case <- c:
                fmt.Println("goroutine exit")
                return
            default: 
            fmt.Println("goroutine running.")
            }
            time.Sleep(100 * time.Millisecond)
        }
    }()

    time.Sleep(1 * time.Second)
    c <- false
    fmt.Println("main fun exit")
}

输出结果

goroutine running.
goroutine running.
goroutine running.
goroutine running.
goroutine running.
goroutine running.
goroutine running.
goroutine running.
goroutine running.
goroutine running.
goroutine exit
main fun exit

这种方法比较优雅,但稍微复杂
如果要控制多个goroutine,或者goroutine嵌套,就比较麻烦

WaitGroup

Go语言提供同步包sync

sync包同步提供基本的同步原语,如互斥锁。除了OnceWaitGroup类型之外,大多数类型都是供低级库例程使用的。通过Channel和沟通可以更好地完成更高级别的同步。并且此包中的值在使用过后不要拷贝。

Sync.WaitGroup是一种实现并发控制方式,WaitGroup 对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。

  • Add(n) 把计数器设置为n
  • Done() 每次把计数器-1
  • wait() 会阻塞代码的运行,直到计数器地值减为0
package main

import (
   "fmt"
   "sync"
   "time"
)

func main() {
    fmt.Println(time.Now().Format("2006-01-02 15:04:05"))

    //定义一个WaitGroup
    var synWait sync.WaitGroup

    //计数器设置为2
    synWait.Add(2)

    go func() {
        time.Sleep(1 * time.Second)
        fmt.Println("goroutine A finish |", time.Now().Format("2006-01-02 15:04:05"))
        
        //计数器减1
        synWait.Done()
    }()

    go func() {
        time.Sleep(3 * time.Second)
        fmt.Println("goroutine B finish |", time.Now().Format("2006-01-02 15:04:05"))
        
        //计数器减1
        synWait.Done()
    }()

    //会阻塞代码的运行,直到计数器地值减为0。
    synWait.Wait()

    fmt.Println("main fun exit |", time.Now().Format("2006-01-02 15:04:05"))
}

输出结果

2019-12-28 01:27:02
goroutine A finish | 2019-12-28 01:27:03
goroutine B finish | 2019-12-28 01:27:05
main fun exit | 2019-12-28 01:27:05

可以看出A在1秒后输出,B在3秒后输出

这种方法适合多个goroutine做同一个,或者差不多事情的时候
因为每个goroutine做的都是这件事情的一部分,只有全部的goroutine都完成,这件事情才算是完成,这是等待的方式。WaitGroup相对于channel并发控制方式比较轻巧

context

应用场景:在 Go http 包的 Server 中,每个Request都需要开启一个goroutine做一些事情,这些goroutine又可能会开启其他的goroutine
所以我们需要一种可以跟踪goroutine的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的Context,称之为上下文

控制并发的实现方式:

  1. context.Background():返回一个空的Context,这个空的Context一般用于整个Context树的根节点
  2. context.WithCancel(context.Background()),创建一个可取消的子Context,然后当作参数传给goroutine使用,这样就可以使用这个子Context跟踪这个goroutine
  3. 在goroutine中,使用select调用<-ctx.Done()判断是否要结束,如果接收到值的话,就可以返回结束goroutine了。如果接收不到,就会继续进行监控
  4. cancel(),取消函数(context.WithCancel()返回的第二个参数,名字和声明的名字一致)。作用是给goroutine发送结束指令
package main

import (
   "fmt"
   "time"
   "context"
)

func main() {
    //创建一个可取消子context,context.Background():返回一个空的Context,这个空的Context一般用于整个Context树的根节点
    ctx, cancel := context.WithCancel(context.Background())
    
    go func(ctx context.Context) {
        for {
            select {
            //使用select调用<-ctx.Done()判断是否要结束
            case <-ctx.Done():
                fmt.Println("goroutine exit")
                return
            default:
                fmt.Println("goroutine running.")
                time.Sleep(100 * time.Millisecond)
            }
        }
    }(ctx)

    time.Sleep(1 * time.Second)
    //取消context
    cancel()
    time.Sleep(1 * time.Second)

    fmt.Println("main fun exit")
}

输出结果

goroutine running.
goroutine running.
goroutine running.
goroutine running.
goroutine running.
goroutine running.
goroutine running.
goroutine running.
goroutine running.
goroutine running.
main fun exit

如果想控制多个goroutine ,也很简单

package main

import (
   "fmt"
   "time"
   "context"
)

func main() {
    //创建一个可取消子context,context.Background():返回一个空的Context,这个空的Context一般用于整个Context树的根节点。
    ctxA, cancelA := context.WithCancel(context.Background())
    ctxB, cancelB := context.WithCancel(context.Background())

    go func(ctx context.Context) {
        for {
            select {
            //使用select调用<-ctx.Done()判断是否要结束
            case <- ctx.Done():
                fmt.Println("goroutine A exit")
                return
            default:
                fmt.Println("goroutine A running.")
                time.Sleep(200 * time.Millisecond)
            }
        }
    }(ctxA)

    go func(ctx context.Context) {
        for {
            select {
            //使用select调用<-ctx.Done()判断是否要结束
            case <- ctx.Done():
                fmt.Println("goroutine B exit")
                return
            default:
                fmt.Println("goroutine B running.")
                time.Sleep(200 * time.Millisecond)
            }
        }
    }(ctxA)

    go func(ctx context.Context) {
        for {
            select {
            //使用select调用<-ctx.Done()判断是否要结束
            case <- ctx.Done():
                fmt.Println("goroutine C exit")
                return
            default:
                fmt.Println("goroutine C running.")
                time.Sleep(200 * time.Millisecond)
            }
        }
    }(ctxB)

    time.Sleep(1 * time.Second)
    //取消context
    cancelA()
    cancelB()
    time.Sleep(1 * time.Second)
    
    fmt.Println("main fun exit")
}

输出结果:

goroutine A running.
goroutine C running.
goroutine B running.
goroutine A running.
goroutine C running.
goroutine B running.
goroutine A running.
goroutine C running.
goroutine B running.
goroutine A running.
goroutine C running.
goroutine B running.
goroutine A running.
goroutine C running.
goroutine B running.
goroutine A exit
goroutine C exit
goroutine B exit
main fun exit

至于context的具体用法,请参考深度解密Go语言之context

参考文档

Go goroutine理解
Go channel 实现原理分析
深度解密Go语言之context
Go 并发控制
GO语言并发编--sync包之WaitGroup的使用
Go Context的踩坑经历

Last modification:August 11, 2020
If you think my article is useful to you, please feel free to appreciate