Go 并发控制

栏目: Go · 发布时间: 6年前

内容简介:提到Go语言的并发,就不得不提goroutine,其作为Go语言的一大特色,在日常开发中使用很多。在日常应用场景就会涉及一个goroutine启动或结束,启动一个goroutine很简单只需要在函数前面加关键词go即可,而由于每个goroutine都是独立运行的,其退出有自身决定的,除非main主程序结束或程序崩溃的情况发生。那么,如何控制goroutine或者说通知goroutine结束运行呢?

前言

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

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

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

解决的方式其实很简单,那就是想办法和goroutine通讯,通知goroutine什么时候结束,goroutine结束也可以通知其他goroutine或main主程序。

并发控制方法主要有:

全局变量

channel

WaitGroup

context

全局变量

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

1、声明一个全局变量。

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

3、在主进程中变更该全局变量;

4、子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/...

1、声明一个 stop 的chan。

2、在goroutine中,使用select判断 stop 是否可以接收到值,如果可以接收到,就表示可以退出停止了;如果没有接收到,就会执行 default 里逻辑。直到收到 stop 的通知。

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

4、子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并发控制方式比较轻巧。

注意:

1、计数器不能为负值

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

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"
   "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


以上所述就是小编给大家介绍的《Go 并发控制》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Data Structures and Algorithms in Java

Data Structures and Algorithms in Java

Robert Lafore / Sams / 2002-11-06 / USD 64.99

Data Structures and Algorithms in Java, Second Edition is designed to be easy to read and understand although the topic itself is complicated. Algorithms are the procedures that software programs use......一起来看看 《Data Structures and Algorithms in Java》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器