Golang 协程并发控制的几种方式
goroutine 协成的并发能提高程序的运行效率,在 IO 密集型的操作中,能大幅提高程序的运行效率
goroutine 的并发控制,主要可以通过以下几种方式
全局变量
这是并发控制最简单的实现方式,优点是实现比较简单,缺点是适用一些逻辑简单的场景,全局变量的信息较少,为了防止不同 goroutine 同时修改全局变量,需要加锁来解决
1、声明一个全局变量。
2、所有子 goroutine 共享这个变量,并不断轮询这个变量检查是否有更新;
3、在 main 协程中变更该全局变量;
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(3 * time.Second)
open = false
//等待子协程完成
time.Sleep(1 * time.Second)
fmt.Println("main fun exit")
}
运行结果
goroutineA running
goroutineB running
goroutineA running
goroutineB running
goroutineA running
goroutineB running
goroutineB exit
goroutineA exit
main fun exit
channel 通道
channel 是 goroutine 之间常见的通信方式,通过它可以在 goroutine 之间发送和接收消息。它是 Golang 在语言层面提供的 goroutine 间的通信方式。一般会和 select 搭配使用。
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(3 * time.Second)
stop <- true
time.Sleep(1 * time.Second)
fmt.Println("main fun exit")
}
运行结果
goroutine running
goroutine running
goroutine running
goroutine exit
main fun exit
WaitGroup
waitgroup 由 sync 包提供,Sync.WaitGroup 是一种实现并发控制方式,WaitGroup 对象内部有一个计数器,最初从 0 开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。
Add(n) 把计数器设置为 n 。 Done() 每次把计数器-1 。 wait() 会阻塞代码的运行,直到计数器地值减为 0。
这种控制并发的方式适用于,好多个 goroutine 协同做一件事情的时候,因为每个 goroutine 做的都是这件事情的一部分,只有全部的 goroutine 都完成,这件事情才算是完成,这是等待的方式。WaitGroup 相对于 channel 并发控制方式比较轻巧。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
//定义一个WaitGroup
var wg sync.WaitGroup
//计数器设置为2
wg.Add(2)
go func() {
//计数器减1
defer wg.Done()
time.Sleep(2 * time.Second)
fmt.Println("redis task finish")
}()
go func() {
//计数器减1
defer wg.Done()
time.Sleep(1 * time.Second)
fmt.Println("db task finish")
}()
//会阻塞代码的运行,直到计数器地值减为0。
wg.Wait()
fmt.Println("main fun exit")
}
运行结果
db task finish
redis task finish
main fun exit
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(1 * time.Second)
}
}
}(ctx)
time.Sleep(3 * time.Second)
//取消context
cancel()
time.Sleep(1 * time.Second)
fmt.Println("main fun exit")
}
运行结果
goroutine running.
goroutine running.
goroutine running.
goroutine exit
main fun exit