Go 并发控制

前言

提到Go语言的并发,就不得不提goroutine,其做为Go语言的一大特点,在平常开发中使用不少。git

在平常应用场景就会涉及一个goroutine启动或结束,启动一个goroutine很简单只须要在函数前面加关键词go便可,而因为每一个goroutine都是独立运行的,其退出有自身决定的,除非main主程序结束或程序崩溃的状况发生。github

那么,如何控制goroutine或者说通知goroutine结束运行呢?golang

解决的方式其实很简单,那就是想办法和goroutine通信,通知goroutine何时结束,goroutine结束也能够通知其余goroutine或main主程序。并发

并发控制方法主要有:

全局变量函数

channel.net

WaitGroupcode

context对象

全局变量

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

一、声明一个全局变量。接口

二、全部子goroutine共享这个变量,并不断轮询这个变量检查是否有更新;

三、在主进程中变动该全局变量;

四、子goroutine检测到全局变量更新,执行相应的逻辑。

示例

package main

import (
   "fmt"
   "time"
)

func main() {
   open := true
   go func() {
      for open {
         println("goroutineA running")
         time.Sleep(1 * time.Second)
      }
      println("goroutineA exit")
   }()
   go func() {
      for open {
         println("goroutineB running")
         time.Sleep(1 * time.Second)
      }
      println("goroutineB exit")
   }()
   time.Sleep(2 * time.Second)
   open = false
   time.Sleep(2 * time.Second)
   fmt.Println("main fun exit")
}

输出

goroutineA running
goroutineB running
goroutineA running
goroutineB running
goroutineB running
goroutineA exit
goroutineB exit
main fun exit

这种实现方式

优势:实现简单。

缺点:适用一些逻辑简单的场景,全局变量的信息量比较少,为了防止不一样goroutine同时修改变量须要用到加锁来解决。

channel

channel是goroutine之间主要的通信方式,通常会和select搭配使用。

如想了解channel实现原理可参考

https://github.com/guyan0319/...

一、声明一个stop的chan。

二、在goroutine中,使用select判断stop是否能够接收到值,若是能够接收到,就表示能够退出中止了;若是没有接收到,就会执行default里逻辑。直到收到stop的通知。

三、主程序发送了stop<- true结束的指令后。

四、子goroutine接到结束指令case <-stop退出return。

示例

package main

import (
   "fmt"
   "time"
)

func main() {
   stop := make(chan bool)
   go func() {
      for {
         select {
         case <-stop:
            fmt.Println("goroutine exit")
            return
         default:
            fmt.Println("goroutine running")
            time.Sleep(1 * time.Second)
         }
      }
   }()
   time.Sleep(2 * time.Second)
   stop <- true
   time.Sleep(2 * time.Second)
   fmt.Println("main fun exit")
}

输出

goroutine running
goroutine running
goroutine running
goroutine exit
main fun exit

这种select+chan是一种比较优雅的并发控制方式,但也有局限性,如多个goroutine 须要结束,以及嵌套goroutine 的场景。

WaitGroup

Go语言提供同步包(sync),源码(src/sync/waitgroup.go)。

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

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

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

示例

package main

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

func main() {
   //定义一个WaitGroup
   var wg sync.WaitGroup
   //计数器设置为2
   wg.Add(2)
   go func() {
      time.Sleep(2 * time.Second)
      fmt.Println("goroutineA finish")
      //计数器减1
      wg.Done()
   }()
   go func() {
      time.Sleep(2 * time.Second)
      fmt.Println("goroutineB finish")
      //计数器减1
      wg.Done()
   }()
   //会阻塞代码的运行,直到计数器地值减为0。
   wg.Wait()
   time.Sleep(2 * time.Second)
   fmt.Println("main fun exit")
}

这种控制并发的方式适用于,好多个goroutine协同作一件事情的时候,由于每一个goroutine作的都是这件事情的一部分,只有所有的goroutine都完成,这件事情才算是完成,这是等待的方式。WaitGroup相对于channel并发控制方式比较轻巧。

注意:

一、计数器不能为负值

二、WaitGroup对象不是一个引用类型

Context

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

控制并发的实现方式:

一、 context.Background():返回一个空的Context,这个空的Context通常用于整个Context树的根节点。

二、context.WithCancel(context.Background()),建立一个可取消的子Context,而后看成参数传给goroutine使用,这样就可使用这个子Context跟踪这个goroutine。

三、在goroutine中,使用select调用<-ctx.Done()判断是否要结束,若是接收到值的话,就能够返回结束goroutine了;若是接收不到,就会继续进行监控。

四、cancel(),取消函数(context.WithCancel()返回的第二个参数,名字和声明的名字一致)。做用是给goroutine发送结束指令。

示例:

package main

import (
   "fmt"
   "time"
   "golang.org/x/net/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(2 * time.Second)
         }
      }
   }(ctx)

   time.Sleep(10 * time.Second)
   fmt.Println("main fun exit")
   //取消context
   cancel()
   time.Sleep(5 * time.Second)

}

输出:

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

若是想控制多个goroutine ,也很简单。

示例

package main

import (
   "fmt"
   "time"
   "golang.org/x/net/context"
)

func main() {
   //建立一个可取消子context,context.Background():返回一个空的Context,这个空的Context通常用于整个Context树的根节点。
   ctx, cancel := context.WithCancel(context.Background())
   ctxTwo, cancelTwo := context.WithCancel(context.Background())
   go func(ctx context.Context) {
      for {
         select {
         //使用select调用<-ctx.Done()判断是否要结束
         case <-ctx.Done():
            fmt.Println("goroutineA exit")
            return
         default:
            fmt.Println("goroutineA running.")
            time.Sleep(2 * time.Second)
         }
      }
   }(ctx)
   go func(ctx context.Context) {
      for {
         select {
         //使用select调用<-ctx.Done()判断是否要结束
         case <-ctx.Done():
            fmt.Println("goroutineB exit")
            return
         default:
            fmt.Println("goroutineB running.")
            time.Sleep(2 * time.Second)
         }
      }
   }(ctx)
   go func(ctxTwo context.Context) {
      for {
         select {
         //使用select调用<-ctx.Done()判断是否要结束
         case <-ctxTwo.Done():
            fmt.Println("goroutineC exit")
            return
         default:
            fmt.Println("goroutineC running.")
            time.Sleep(2 * time.Second)
         }
      }
   }(ctxTwo)

   time.Sleep(4 * time.Second)
   fmt.Println("main fun exit")
   //取消context
   cancel()
   cancelTwo()
   time.Sleep(5 * time.Second)

}

结果:

goroutineA running.
goroutineB running.
goroutineC running.
goroutineB running.
goroutineC running.
goroutineA running.
goroutineC running.
goroutineA running.
goroutineB running.
main fun exit
goroutineC exit
goroutineA exit
goroutineB exit

context还适用于更复杂的场景,如主动取消goroutine或goroutine定时取消等。context接口除了func WithCancel(parent Context) (ctx Context, cancel CancelFunc),还有衍生如下方法

  • func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc):
    此函数返回其父项的派生 context,当截止日期超过或取消函数被调用时,该 context 将被取消。例如,您能够建立一个将在之后的某个时间自动取消的 context,并在子函数中传递它。当由于截止日期耗尽而取消该 context 时,获此 context 的全部函数都会收到通知去中止运行并返回。
  • func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc):

    此函数相似于 context.WithDeadline。不一样之处在于它将持续时间做为参数输入而不是时间对象。此函数返回派生 context,若是调用取消函数或超出超时持续时间,则会取消该派生 context。

  • func WithValue(parent Context, key, val interface{}) Context:

    此函数接收 context 并返回派生 context,其中值 val 与 key 关联,并经过 context 树与 context 一块儿传递。这意味着一旦得到带有值的 context,从中派生的任何 context 都会得到此值。不建议使用 context 值传递关键参数,而是函数应接收签名中的那些值,使其显式化。

有兴趣的同窗请阅读:https://studygolang.com/pkgdoc

参考:

https://tutorialedge.net/gola...

http://goinbigdata.com/golang...

https://medium.com/code-zen/c...

https://blog.csdn.net/u013029...

links

相关文章
相关标签/搜索