Go语言里的并发控制

在Go语言编写的程序里可能会开启多个goroutine, 这样在主 goroutine 结束前可能在其它的goroutine里,还有任务在继续运行,这样的话,我们必须堵塞主 goroutine 才能保证其它 goroutine 正常运行。但是如何在并发的程序里合理地编写代码呢?Go 语言大概为我们提供了这几种方式。

普通的方法

首先定义一个存储类型为 struct{} 的 channel, 这样在main 函数里的末尾接收 channel 里的消息,当 channel 为空时便会堵塞住,直到在另外一个goroutine 里向 这个 channel 发送了消息,main 函数里的 channel 收到消息,程序运行结束。

1
2
3
4
5
6
7
8
9
10
func main() {
done := make(chan struct{})
go func() {
fmt.Println("I've finished!")
done <- struct{}{} //或者close(done)
}()
<- done
}

使用sync.WaitGroup

WaitGroup 会堵塞主线程的执行,一直到其它 goroutine 的任务都执行完成。在WaitGroup里主要有三个方法

  • Add, 可以添加或减少 goroutine的数量

  • Done, 相当于Add(-1)

  • Wait, 执行后会堵塞主线程,直到WaitGroup 里的值减至0

先声明一个全局的WaitGroup 和一个全局int32类型的变量count。

1
2
var wg sync.WaitGroup
var count int32

定义一个函数,在函数里调用wg.Done,调用一次相当于执行一次Add(-1)

1
2
3
4
5
/*定义一个函数使一个数加1*/
func AddOne() {
defer wg.Done()
count++
}

main 里的代码,开启三个goroutine, 最后打印出count 的值,执行结果为3 。

1
2
3
4
5
6
7
8
9
func main() {
wg.Add(3)
go AddOne()
go AddOne()
go AddOne()
wg.Wait()
fmt.Printf("Count: %d", count )
}

WaitGroup 实现了一个类似队列的结构,我们可以一直向队列中添加任务,完成一个任务后便从队列中删除一个任务,如果队列中的任务没有完全完成,可以通过Wait()方法来阻塞住主线程,等待其它 goroutine的任务一一执行完成。

Notice: A WaitGroup must not be copied after first use.

使用context.Context

Context 即上下文,它是包括一个程序的运行环境、现场和快照等。每个程序要运行时,都需要知道当前程序的运行状态,通常Go 将这些封装在一个Context 里,再将它传给要执行的 goroutine 。context 包主要是用来处理多个 goroutine 之间共享数据,及多个goroutine 的管理。

context包里的Context接口:

1
2
3
4
5
6
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}

Deadline 方法返回一个超时时间,超时后这个Context 会被取消;Err 方法输出出错日志,即Context取消的原因; Done 方法返回一个 channel,当Context 超时后,此channel 会被关闭; Value 方法用以不同goroutine 间的共享数据,不过使用时要注意同步问题。

使用Context 时,我们在main 函数里可以制定了一个deadline, 在这个deadline 来临前,其它的goroutine 都可以正常地运行,到达 deadline后程序立即结束,比如让主线程睡眠一段时间。

上代码,同样声明一个int32 类型的变量count。

1
var count int32

这次定义一个函数,当函数运行时,不断轮循调用方法 Done,这个方法返回<- struct{}, 通过这种方式可以查看main函数里是否调用了cancel 函数。

1
2
3
4
5
6
7
8
9
10
11
12
/*定义一个函数使一个数不断地加1*/
func AddOneContinually(ctx context.Context) {
for {
select{
case <- ctx.Done():
return
default:
count++
}
}
}

Backgroud方法返回一个非nil的空的Context, 它不能被取消、没有值,也没有过期时间。通常需要我们在main 函数里初始化,通过WithCancel 方法从父Context 创建一个衍生的子Context ,然后将其返回,同时返回一个函数指针 。此外,创建子Context 的方法还有WithDeadline,WithTimeout和WithValue。

WithCancel 返回的函数指针类型为:

1
type CancelFunc func()

调用这个函数可以结束Context, 当一个Context 结束后,在其它 goroutine 里,如上述AddOneContinually 函数里的select 语句里,发现Context 已经结束,于是这个goroutine 马上终止自己。

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
go AddOneContinually(ctx)
go AddOneContinually(ctx)
go AddOneContinually(ctx)
time.Sleep(time.Microsecond * 3 )
cancel()
fmt.Printf("Count: %d", count )
}

另外,要注意的是,不要随便传一个nil的Context 给其它 goroutine。当不确定要使用哪个类型的Context时,可以使用context.TODO(),它同样返回一个非nil的空的Context。使用context的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数。

参考资料: