如何主动关闭goroutine

在学习go语言channel中,读到最多的一句话便是通过通信共享内存,而不是通过共享内存来进行通信(Do not communicate by sharing memory;instead,share memory by communicating.)那么如何来理解这句话呢?个人理解通过共享内存来通信的话必须保证数据的安全及正确性,即需要通过加锁等手段进行控制,但是此种方式带来的性能开销以及可能造成的死锁问题处理起来较为繁琐。而go语言则通过channel来通信,通过通信来传递内存数据,个人认为更加优雅简洁与高效。
很多情况下我们需要主动关闭goroutine,那么如何实现呢?

使用channel进行控制

for-range结构

for-range从channel上获取值,直到channel关闭。该结构对于从单一通道上获取数据去执行某些任务是十分方便的,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func producer(out chan<- int) {
for i := 0; i < 10; i++ {
data := i * i
fmt.Println("生产者生产数据:", data)
out <- data
}
close(out)
}

func consumer(in <-chan int) {
for data := range in {
fmt.Println("消费者得到数据:", data)
}
}

func main() {
ch := make(chan int)
go producer(ch)
consumer(ch)
}

for-select结构

当channel比较多时,for-range结构就不是很方便了。此时可以使用for-select,select能够让goroutine在多个通信操作上等待(可以理解为监听多个channel)。

指定一个退出的channel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func producer(out chan<- int, exit chan struct{}) {
for i := 0; i < 10; i++ {
data := i * i
fmt.Println("生产者生产数据:", data)
out <- data
}
exit <- struct{}{}
}

func consumer(in <-chan int, exit chan struct{}) {
for {
select {
case <-exit:
fmt.Println("收到退出信号")
// 不建议使用goto语句
return // 必须return, 否则goroutine不会结束
case data := <-in:
fmt.Println("消费者得到数据:", data)
}
}
}

func main() {
ch := make(chan int)
exitCh := make(chan struct{})
go producer(ch, exitCh)
consumer(ch, exitCh)
}

多个channel都关闭才能退出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
func producer(out1, out2 chan<- int) {
for i := 0; i < 10; i++ {
data := i * i
fmt.Println("生产者生产数据:", data)
out1 <- data
time.Sleep(1 * time.Second)
out2 <- data * 2
}
close(out1)
close(out2)
}

func consumer(in1, in2 <-chan int) {
for {
select {
case data, ok := <-in1:
if !ok {
fmt.Println("收到ch1关闭信号")
in1 = nil
}
fmt.Println("消费者得到数据:", data)
case data, ok := <-in2:
if !ok {
fmt.Println("收到ch2关闭信号")
in2 = nil
}
fmt.Println("消费者得到数据:", data)
}
if in1 == nil && in2 == nil {
return
}
}
}

func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go producer(ch1, ch2)
consumer(ch1, ch2)
}

使用content包进行控制

context是官方提供的用于控制多个goroutine协作的包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
func producer(out chan<- int, ctx context.Context, cancel context.CancelFunc) {
for i := 0; i < 10; i++ {
data := i * i
fmt.Println("生产者生产数据:", data)
out <- data
}
subCtx, _ := context.WithCancel(ctx)
go consumer2(subCtx)
cancel()
}

func consumer(in <-chan int, ctx context.Context) {
for {
select {
case data := <-in:
fmt.Println("消费者得到数据:", data)
case <-ctx.Done():
fmt.Println("收到结束信号")
return // 必须return, 防止goroutine泄露
}
}
}

func consumer2(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("收到结束信号, consumer2")
return // 必须return, 防止goroutine泄露
}
}
}

func main() {
ctx, cancel := context.WithCancel(context.Background())
ch := make(chan int)
go producer(ch, ctx, cancel)
consumer(ch, ctx)
time.Sleep(1 * time.Second)
}

总结

在实际开发过程中,不会简单的启动goroutine就结束了。往往是需要有效的管理多个goroutine之间的协作,此时掌握如何主动关闭goroutine就显得尤为重要了。不仅如此,还需要学会分析go语言运行性能,下一个目标就是学习go语言中的性能大杀器pprof。