网站首页 > 博客文章 正文
前言
提到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
- 目录
本文来自:Segmentfault
感谢作者:guyan0319
原文:Go 并发控制 https://segmentfault.com/a/1190000019229973?utm_campaign=studygolang.com&utm_medium=studygolang.com&utm_source=studygolang.com
猜你喜欢
- 2024-10-03 关于go语言中的协程相关知识点总结(三)
- 2024-10-03 详解MS SQL日常维护管理脚本--第二部分
- 2024-10-03 go语言goroutine调度原理以及channel详解
- 2024-10-03 SQL SERVER:索引概述及创建与使用(41)
- 2024-10-03 SQL Server 联接概述(sqlserver连接工具有哪些)
- 2024-10-03 从OOM的角度,带你了解虚拟内存机制
- 2024-10-03 golang开发:select多路选择(golang elseif)
- 2024-10-03 从0开始学Golang编程-基础语法(golang语言基础)
- 2024-10-03 非阻塞的Go channel(非阻塞的症状)
- 2024-10-03 数据库加锁原理举例说明(数据库加锁解锁)
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- ifneq (61)
- 字符串长度在线 (61)
- googlecloud (64)
- messagesource (56)
- promise.race (63)
- 2019cad序列号和密钥激活码 (62)
- window.performance (66)
- qt删除文件夹 (72)
- mysqlcaching_sha2_password (64)
- ubuntu升级gcc (58)
- nacos启动失败 (64)
- ssh-add (70)
- jwt漏洞 (58)
- macos14下载 (58)
- yarnnode (62)
- abstractqueuedsynchronizer (64)
- source~/.bashrc没有那个文件或目录 (65)
- springboot整合activiti工作流 (70)
- jmeter插件下载 (61)
- 抓包分析 (60)
- idea创建mavenweb项目 (65)
- vue回到顶部 (57)
- qcombobox样式表 (68)
- tomcatundertow (58)
- pastemac (61)
本文暂时没有评论,来添加一个吧(●'◡'●)