首先将等待go协程和`done`通道分开。
使用`sync.WaitGroup`来协调goroutines。
func main() {
wait := &sync.WaitGroup{}
N := 3
wait.Add(N)
for i := 1; i <= N; i++ {
go goFunc(wait, i, true)
}
wait.Wait()
fmt.Println(`Exiting main`)
}
每个 goroutine 都会像这样:
func goFunc(wait *sync.WaitGroup, i int, closer bool) {
defer wait.Done()
defer fmt.Println(`Exiting `, i)
T := time.Tick(time.Duration(100*i) * time.Millisecond)
for {
select {
case <-T:
fmt.Println(`Tick `, i)
if closer {
return
}
}
}
}
(https://play.golang.org/p/mDO4P56lzBU)
我们的主要功能是在退出之前成功等待goroutine退出。每个goroutine都在关闭自己,我们希望有一种方法可以同时取消所有的goroutine。
我们将使用一个chan,并利用从通道接收的这个特性:
引用:在关闭的通道上进行接收操作始终可以立即进行,接收先前发送的任何值后,产生元素类型的零值。(参考链接:https://golang.org/ref/spec#Receive_operator)
我们修改我们的goroutine以检查CLOSE:
func goFunc(wait *sync.WaitGroup, i int, closer bool, CLOSE chan struct{}) {
defer wait.Done()
defer fmt.Println(`Exiting `, i)
T := time.Tick(time.Duration(100*i) * time.Millisecond)
for {
select {
case <-CLOSE:
return
case <-T:
fmt.Println(`Tick `, i)
if closer {
close(CLOSE)
}
}
}
}
然后我们改变我们的
func main
函数,使其通过CLOSE通道,并设置
closer
变量,以便只有最后一个goroutine会触发关闭:
func main() {
wait := &sync.WaitGroup{}
N := 3
CLOSE := make(chan struct{})
wait.Add(N)
for i := 1; i <= N; i++ {
go goFunc(wait, i, i == N, CLOSE)
}
wait.Wait()
fmt.Println(`Exiting main`)
}
(https://play.golang.org/p/E91CtRAHDp2)
现在看起来一切都正常。
但事实并非如此。
并发很难。这段代码中潜藏着一个错误,等着在生产环境中咬你。让我们找出它。
修改我们的示例,使
每个 goroutine 都会关闭:
func main() {
wait := &sync.WaitGroup{}
N := 3
CLOSE := make(chan struct{})
wait.Add(N)
for i := 1; i <= N; i++ {
go goFunc(wait, i, true , CLOSE)
}
wait.Wait()
fmt.Println(`Exiting main`)
}
将 goroutine 改为在关闭之前需要一段时间。我们希望两个 goroutine 在同一时间关闭:
func goFunc(wait *sync.WaitGroup, i int, closer bool, CLOSE chan struct{}) {
defer wait.Done()
defer fmt.Println(`Exiting `, i)
T := time.Tick(time.Duration(100*i) * time.Millisecond)
for {
select {
case <-CLOSE:
return
case <-T:
fmt.Println(`Tick `, i)
if closer {
time.Sleep(time.Second)
close(CLOSE)
}
}
}
}
(https://play.golang.org/p/YHnbDpnJCks)
我们撞击了:
Tick 1
Tick 2
Tick 3
Exiting 1
Exiting 2
panic: close of closed channel
goroutine 7 [running]:
main.goFunc(0x40e020, 0x2, 0x68601, 0x430080)
/tmp/sandbox558886627/prog.go:24 +0x2e0
created by main.main
/tmp/sandbox558886627/prog.go:38 +0xc0
Program exited: status 2.
当在一个关闭的通道上接收时会立即返回,但是你无法关闭一个已经关闭的通道。
我们需要一些协调。我们可以使用 `sync.Mutex` 和一个布尔值来指示我们是否已经关闭了通道。让我们创建一个结构体来完成这个任务:
type Close struct {
C chan struct{}
l sync.Mutex
closed bool
}
func NewClose() *Close {
return &Close {
C: make(chan struct{}),
}
}
func (c *Close) Close() {
c.l.Lock()
if (!c.closed) {
c.closed=true
close(c.C)
}
c.l.Unlock()
}
重新编写我们的gofunc和main函数以使用我们的新Close结构,然后我们就可以开始了:
https://play.golang.org/p/eH3djHu8EXW
并发的问题在于你总是需要考虑如果另一个“线程”在代码的任何其他地方会发生什么。