在Go语言编写的程序里可能会开启多个goroutine, 这样在主 goroutine 结束前可能在其它的goroutine里,还有任务在继续运行,这样的话,我们必须堵塞主 goroutine 才能保证其它 goroutine 正常运行。但是如何在并发的程序里合理地编写代码呢?Go 语言大概为我们提供了这几种方式。
普通的方法
首先定义一个存储类型为 struct{} 的 channel, 这样在main 函数里的末尾接收 channel 里的消息,当 channel 为空时便会堵塞住,直到在另外一个goroutine 里向 这个 channel 发送了消息,main 函数里的 channel 收到消息,程序运行结束。
|
|
使用sync.WaitGroup
WaitGroup 会堵塞主线程的执行,一直到其它 goroutine 的任务都执行完成。在WaitGroup里主要有三个方法
Add, 可以添加或减少 goroutine的数量
Done, 相当于Add(-1)
Wait, 执行后会堵塞主线程,直到WaitGroup 里的值减至0
先声明一个全局的WaitGroup 和一个全局int32类型的变量count。
|
|
定义一个函数,在函数里调用wg.Done,调用一次相当于执行一次Add(-1)
|
|
main 里的代码,开启三个goroutine, 最后打印出count 的值,执行结果为3 。
|
|
WaitGroup 实现了一个类似队列的结构,我们可以一直向队列中添加任务,完成一个任务后便从队列中删除一个任务,如果队列中的任务没有完全完成,可以通过Wait()方法来阻塞住主线程,等待其它 goroutine的任务一一执行完成。
Notice: A WaitGroup must not be copied after first use.
使用context.Context
Context 即上下文,它是包括一个程序的运行环境、现场和快照等。每个程序要运行时,都需要知道当前程序的运行状态,通常Go 将这些封装在一个Context 里,再将它传给要执行的 goroutine 。context 包主要是用来处理多个 goroutine 之间共享数据,及多个goroutine 的管理。
context包里的Context接口:
|
|
Deadline 方法返回一个超时时间,超时后这个Context 会被取消;Err 方法输出出错日志,即Context取消的原因; Done 方法返回一个 channel,当Context 超时后,此channel 会被关闭; Value 方法用以不同goroutine 间的共享数据,不过使用时要注意同步问题。
使用Context 时,我们在main 函数里可以制定了一个deadline, 在这个deadline 来临前,其它的goroutine 都可以正常地运行,到达 deadline后程序立即结束,比如让主线程睡眠一段时间。
上代码,同样声明一个int32 类型的变量count。
|
|
这次定义一个函数,当函数运行时,不断轮循调用方法 Done,这个方法返回<- struct{}, 通过这种方式可以查看main函数里是否调用了cancel 函数。
|
|
Backgroud方法返回一个非nil的空的Context, 它不能被取消、没有值,也没有过期时间。通常需要我们在main 函数里初始化,通过WithCancel 方法从父Context 创建一个衍生的子Context ,然后将其返回,同时返回一个函数指针 。此外,创建子Context 的方法还有WithDeadline,WithTimeout和WithValue。
WithCancel 返回的函数指针类型为:
|
|
调用这个函数可以结束Context, 当一个Context 结束后,在其它 goroutine 里,如上述AddOneContinually 函数里的select 语句里,发现Context 已经结束,于是这个goroutine 马上终止自己。
|
|
另外,要注意的是,不要随便传一个nil的Context 给其它 goroutine。当不确定要使用哪个类型的Context时,可以使用context.TODO(),它同样返回一个非nil的空的Context。使用context的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数。
参考资料: