├── README.md ├── detect_close_channel └── select.go ├── detect_close_channel_v2 └── select.go ├── select.go └── stop_channel └── select.go /README.md: -------------------------------------------------------------------------------- 1 | Golang并发模型:合理退出并发协程 2 | ===== 3 | 4 | ## 目录说明 5 | 6 | - master:使用stop通道,主动告知goroutine退出 7 | - stop_channel:和master相同 8 | - detect_close_channel: 示例可以使用for-range替代for-select,range能检测通道关闭,自动退出 9 | - detect_close_channel_v2:在前一个基础上,增加了监控功能,必须使用for-select,使用ok方法检测通道关闭,退出协程 10 | 11 | ------------ 12 | 13 | goroutine作为Golang并发的核心,我们不仅要关注它们的创建和管理,当然还要关注如何合理的退出这些协程,不(合理)退出不然可能会造成阻塞、panic、程序行为异常、数据结果不正确等问题。这篇文章介绍,如何合理的退出goroutine,减少软件bug。 14 | 15 | goroutine在退出方面,不像线程和进程,不能通过某种手段**强制**关闭它们,只能等待goroutine主动退出。但也无需为退出、关闭goroutine而烦恼,下面就介绍3种优雅退出goroutine的方法,只要采用这种最佳实践去设计,基本上就可以确保goroutine退出上不会有问题,尽情享用。 16 | 17 | ### 1:使用for-range退出 18 | 19 | `for-range`是使用频率很高的结构,常用它来遍历数据,**`range`能够感知channel的关闭,当channel被发送数据的协程关闭时,range就会结束**,接着退出for循环。 20 | 21 | 22 | 23 | 它在并发中的使用场景是:当协程只从1个channel读取数据,然后进行处理,处理后协程退出。下面这个示例程序,当in通道被关闭时,协程可自动退出。 24 | 25 | ```go 26 | go func(in <-chan int) { 27 | // Using for-range to exit goroutine 28 | // range has the ability to detect the close/end of a channel 29 | for x := range in { 30 | fmt.Printf("Process %d\n", x) 31 | } 32 | }(inCh) 33 | ``` 34 | 35 | ### 2:使用,ok退出 36 | 37 | `for-select`也是使用频率很高的结构,select提供了多路复用的能力,所以for-select可以让函数具有持续多路处理多个channel的能力。**但select没有感知channel的关闭,这引出了2个问题**: 38 | 1. 继续在关闭的通道上读,会读到通道传输数据类型的零值,如果是指针类型,读到nil,继续处理还会产生nil。 39 | 2. 继续在关闭的通道上写,将会panic。 40 | 41 | 问题2可以这样解决,通道只由发送方关闭,接收方不可关闭,即某个写通道只由使用该select的协程关闭,select中就不存在继续在关闭的通道上写数据的问题。 42 | 43 | 问题1可以使用`,ok`来检测通道的关闭,使用情况有2种。 44 | 45 | 第一种:**如果某个通道关闭后,需要退出协程,直接return即可**。示例代码中,该协程需要从in通道读数据,还需要定时打印已经处理的数量,有2件事要做,所有不能使用for-range,需要使用for-select,当in关闭时,`ok=false`,我们直接返回。 46 | 47 | ```go 48 | go func() { 49 | // in for-select using ok to exit goroutine 50 | for { 51 | select { 52 | case x, ok := <-in: 53 | if !ok { 54 | return 55 | } 56 | fmt.Printf("Process %d\n", x) 57 | processedCnt++ 58 | case <-t.C: 59 | fmt.Printf("Working, processedCnt = %d\n", processedCnt) 60 | } 61 | } 62 | }() 63 | ``` 64 | 65 | 第二种:如果**某个通道关闭了,不再处理该通道,而是继续处理其他case**,退出是等待所有的可读通道关闭。我们需要**使用select的一个特征:select不会在nil的通道上进行等待**。这种情况,把只读通道设置为nil即可解决。 66 | 67 | ```go 68 | go func() { 69 | // in for-select using ok to exit goroutine 70 | for { 71 | select { 72 | case x, ok := <-in1: 73 | if !ok { 74 | in1 = nil 75 | } 76 | // Process 77 | case y, ok := <-in2: 78 | if !ok { 79 | in2 = nil 80 | } 81 | // Process 82 | case <-t.C: 83 | fmt.Printf("Working, processedCnt = %d\n", processedCnt) 84 | } 85 | 86 | // If both in channel are closed, goroutine exit 87 | if in1 == nil && in2 == nil { 88 | return 89 | } 90 | } 91 | }() 92 | ``` 93 | 94 | ### 3:使用退出通道退出 95 | 96 | **使用`,ok`来退出使用for-select协程,解决是当读入数据的通道关闭时,没数据读时程序的正常结束**。想想下面这2种场景,`,ok`还能适用吗? 97 | 98 | 1. 接收的协程要退出了,如果它直接退出,不告知发送协程,发送协程将阻塞。 99 | 2. 启动了一个工作协程处理数据,如何通知它退出? 100 | 101 | **使用一个专门的通道,发送退出的信号,可以解决这类问题**。以第2个场景为例,协程入参包含一个停止通道`stopCh`,当`stopCh`被关闭,`case <-stopCh`会执行,直接返回即可。 102 | 103 | 当我启动了100个worker时,只要`main()`执行关闭stopCh,每一个worker都会都到信号,进而关闭。如果`main()`向stopCh发送100个数据,这种就低效了。 104 | 105 | ```go 106 | func worker(stopCh <-chan struct{}) { 107 | go func() { 108 | defer fmt.Println("worker exit") 109 | // Using stop channel explicit exit 110 | for { 111 | select { 112 | case <-stopCh: 113 | fmt.Println("Recv stop signal") 114 | return 115 | case <-t.C: 116 | fmt.Println("Working .") 117 | } 118 | } 119 | }() 120 | return 121 | } 122 | ``` 123 | 124 | ### 最佳实践回顾 125 | 126 | 1. 发送协程主动关闭通道,接收协程不关闭通道。技巧:把接收方的通道入参声明为只读,如果接收协程关闭只读协程,编译时就会报错。 127 | 2. 协程处理1个通道,并且是读时,协程优先使用`for-range`,因为`range`可以关闭通道的关闭自动退出协程。 128 | 3. `,ok`可以处理多个读通道关闭,需要关闭当前使用`for-select`的协程。 129 | 4. 显式关闭通道`stopCh`可以处理主动通知协程退出的场景。 130 | 131 | ### 完整示例代码 132 | 133 | 本文所有代码都在仓库,可查看完整示例代码:https://github.com/Shitaibin/golang_goroutine_exit 134 | 135 | ### 并发系列文章推荐 136 | 137 | - [Golang并发模型:轻松入门流水线模型](https://mp.weixin.qq.com/s?__biz=Mzg3MTA0NDQ1OQ==&mid=2247483671&idx=1&sn=1706ffa6deee44a367c34ef84448f55f&scene=21#wechat_redirect) 138 | - [Golang并发模型:轻松入门流水线FAN模式](https://mp.weixin.qq.com/s?__biz=Mzg3MTA0NDQ1OQ==&mid=2247483680&idx=1&sn=de463ebbd088c0acf6c2f0b5f179f38d&scene=21#wechat_redirect) 139 | - [Golang并发模型:并发协程的优雅退出](https://mp.weixin.qq.com/s/RjomKnfwCTy7tC9gbpPxCQ) 140 | - [Golang并发模型:轻松入门select](https://mp.weixin.qq.com/s/ACh-TGlPo72r4e6pbh52vg) 141 | 142 | > 1. 如果这篇文章对你有帮助,请点个赞/喜欢,鼓励我持续分享,感谢。 143 | > 2. [我的文章列表,点此可查看](http://lessisbetter.site/2018/12/11/gongzhonghao-articles/) 144 | > 3. 如果喜欢本文,随意转载,但请保留此[原文链接](https://mp.weixin.qq.com/s/RjomKnfwCTy7tC9gbpPxCQ)。 145 | 146 | 147 | ![一起学Golang-分享有料的Go语言技术](http://cdn.lessisbetter.site/image/png/gzh/gzh-%E5%B8%A6%E5%AD%97%E4%BA%8C%E7%BB%B4%E7%A0%81.png) -------------------------------------------------------------------------------- /detect_close_channel/select.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func producer(n int) <-chan int { 9 | out := make(chan int) 10 | go func() { 11 | defer func() { 12 | close(out) 13 | out = nil 14 | fmt.Println("producer exit") 15 | }() 16 | 17 | for i := 0; i < n; i++ { 18 | fmt.Printf("send %d\n", i) 19 | out <- i 20 | time.Sleep(time.Millisecond) 21 | } 22 | }() 23 | return out 24 | } 25 | 26 | // consumer only read data from in channel and print it 27 | func consumer(in <-chan int) <-chan struct{} { 28 | finish := make(chan struct{}) 29 | 30 | go func() { 31 | defer func() { 32 | fmt.Println("worker exit") 33 | finish <- struct{}{} 34 | close(finish) 35 | }() 36 | 37 | // Using for-range to exit goroutine 38 | // range has the ability to detect the close/end of a channel 39 | for x := range in { 40 | fmt.Printf("Process %d\n", x) 41 | } 42 | }() 43 | 44 | return finish 45 | } 46 | 47 | func main() { 48 | out := producer(3) 49 | finish := consumer(out) 50 | 51 | // Wait consumer exit 52 | <-finish 53 | fmt.Println("main exit") 54 | } 55 | 56 | // ➜ golang_for_select git:(detect_close_channel) ✗ go run select.go 57 | // send 0 58 | // Process 0 59 | // send 1 60 | // Process 1 61 | // send 2 62 | // Process 2 63 | // producer exit 64 | // worker exit 65 | // main exit 66 | -------------------------------------------------------------------------------- /detect_close_channel_v2/select.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func producer(n int) <-chan int { 9 | out := make(chan int) 10 | go func() { 11 | defer func() { 12 | close(out) 13 | out = nil 14 | fmt.Println("producer exit") 15 | }() 16 | 17 | for i := 0; i < n; i++ { 18 | fmt.Printf("send %d\n", i) 19 | out <- i 20 | time.Sleep(time.Millisecond) 21 | } 22 | }() 23 | return out 24 | } 25 | 26 | // consumer read data from in channel, print it, and print 27 | // all proccess count in each second 28 | func consumer(in <-chan int) <-chan struct{} { 29 | finish := make(chan struct{}) 30 | 31 | t := time.NewTicker(time.Millisecond * 500) 32 | processedCnt := 0 33 | 34 | go func() { 35 | defer func() { 36 | fmt.Println("worker exit") 37 | finish <- struct{}{} 38 | close(finish) 39 | }() 40 | 41 | // in for-select using ok to exit goroutine 42 | for { 43 | select { 44 | case x, ok := <-in: 45 | if !ok { 46 | return 47 | } 48 | fmt.Printf("Process %d\n", x) 49 | processedCnt++ 50 | case <-t.C: 51 | fmt.Printf("Working, processedCnt = %d\n", processedCnt) 52 | } 53 | } 54 | }() 55 | 56 | return finish 57 | } 58 | 59 | func main() { 60 | out := producer(3) 61 | finish := consumer(out) 62 | 63 | // Wait consumer exit 64 | <-finish 65 | fmt.Println("main exit") 66 | } 67 | 68 | // ➜ go run select.go 69 | // send 0 70 | // Process 0 71 | // send 1 72 | // Process 1 73 | // send 2 74 | // Process 2 75 | // producer exit 76 | // worker exit 77 | // main exit 78 | -------------------------------------------------------------------------------- /select.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func worker(stopCh <-chan struct{}) { 9 | go func() { 10 | defer fmt.Println("worker exit") 11 | 12 | t := time.NewTicker(time.Millisecond * 500) 13 | 14 | // Using stop channel explicit exit 15 | for { 16 | select { 17 | case <-stopCh: 18 | fmt.Println("Recv stop signal") 19 | return 20 | case <-t.C: 21 | fmt.Println("Working .") 22 | } 23 | } 24 | }() 25 | return 26 | } 27 | 28 | func main() { 29 | 30 | stopCh := make(chan struct{}) 31 | worker(stopCh) 32 | 33 | time.Sleep(time.Second * 2) 34 | close(stopCh) 35 | 36 | // Wait some print 37 | time.Sleep(time.Second) 38 | fmt.Println("main exit") 39 | } 40 | 41 | // ➜ golang_for_select git:(master) ✗ go run select.go 42 | // Working . 43 | // Working . 44 | // Working . 45 | // Working . 46 | // Recv stop signal 47 | // worker exit 48 | // main exit 49 | -------------------------------------------------------------------------------- /stop_channel/select.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func worker(stopCh <-chan struct{}) { 9 | go func() { 10 | defer fmt.Println("worker exit") 11 | 12 | t := time.NewTicker(time.Millisecond * 500) 13 | 14 | // Using stop channel explicit exit 15 | for { 16 | select { 17 | case <-stopCh: 18 | fmt.Println("Recv stop signal") 19 | return 20 | case <-t.C: 21 | fmt.Println("Working .") 22 | } 23 | } 24 | }() 25 | return 26 | } 27 | 28 | func main() { 29 | 30 | stopCh := make(chan struct{}) 31 | worker(stopCh) 32 | 33 | time.Sleep(time.Second * 2) 34 | close(stopCh) 35 | 36 | // Wait some print 37 | time.Sleep(time.Second) 38 | fmt.Println("main exit") 39 | } 40 | 41 | // ➜ golang_for_select git:(master) ✗ go run select.go 42 | // Working . 43 | // Working . 44 | // Working . 45 | // Working . 46 | // Recv stop signal 47 | // worker exit 48 | // main exit 49 | --------------------------------------------------------------------------------