最灵繁的人也看不见自己的背脊。——非洲
1 select监听channel
使用select可以监控多channel,比如监控多个channel,当其中某一个channel有数据时,就从其读出数据。
一个简单的示例程序如下:
package main import ( "fmt" "time" ) func addNumberToChan(chanName chan int) { for { chanName <- 1 time.Sleep(1 * time.Second) } } func main() { var chan1 = make(chan int, 10) var chan2 = make(chan int, 10) go addNumberToChan(chan1) go addNumberToChan(chan2) for { select { case e := <- chan1 : fmt.Printf("Get element from chan1: %d\n", e) case e := <- chan2 : fmt.Printf("Get element from chan2: %d\n", e) default: fmt.Printf("No element in chan1 and chan2.\n") time.Sleep(1 * time.Second) } } }
程序中创建两个channel:chan1和chan2。函数addNumberToChan()函数会向两个channel中周期性写入数据。通过select可以监控两个channel,任意一个可读时就从其中读出数据。
程序输出如下:
D:\SourceCode\GoExpert\src>go run main.go Get element from chan1: 1 Get element from chan2: 1 No element in chan1 and chan2. Get element from chan2: 1 Get element from chan1: 1 No element in chan1 and chan2. Get element from chan2: 1 Get element from chan1: 1 No element in chan1 and chan2.
从输出可见,从channel中读出数据的顺序是随机的,事实上select语句的多个case执行顺序是随机的,关于select的实现原理会有专门章节分析。
通过这个示例想说的是:select的case语句读channel不会阻塞,尽管channel中没有数据。这是由于case语句编译后调用读channel时会明确传入不阻塞的参数,此时读不到数据时不会将当前goroutine加入到等待队列,而是直接返回。
2 range
通过range可以持续从channel中读出数据,好像在遍历一个数组一样,当channel中没有数据时会阻塞当前goroutine,与读channel时阻塞处理机制一样。简单流程图如下:
func chanRange(chanName chan int) { for e := range chanName { fmt.Printf("Get element from chan: %d\n", e) } }
注意:如果向此channel写数据的goroutine退出时,系统检测到这种情况后会panic,否则range将会永久阻塞。
3 channel导致goroutine泄漏
3.1 阻塞channel导致的协程泄漏
func examplefunc(handle func(params ...interface{}) (interface{}, error), timeout time.Duration, params ...interface{}) (interface{}, error) { if timeout == time.Duration(0) { timeout = time.Second } timer := time.NewTimer(timeout) finish := make(chan struct{}) defer timer.Stop() var err error var data interface{} go func() { data, err = handle(params...) defer finish <- struct{}{} }() select { case <-timer.C: return nil, error_codes.ErrCallTimeOut case <-finish: return data, err } }
分析:
函数内部启用协程获取handle的返回结果,这里finish作为阻塞管道用于控制后面select的处理,但是当超时或函数内部异常panic时,finish里面的数据无处消费,会导致协程永久阻塞最终导致协程泄漏
结论:
此处将finish修改为非阻塞管道,即finish := make(chan struct{},1)
3.2 3. channel忘记关闭导致的协程泄漏
func bugfunc() { taskChan := make(chan int, 100) for i := 0; i < 100; i++ { taskChan <- i } consumer := func() { for task := range taskChan { fmt.Println(task) } } for i := 0; i < 100; i++ { go consumer() } }
分析:
go语言中并不强制开发者主动关闭channel(channel也是可以被GC的),但是根据内存回收机制这里的taskChan不会被GC,导致协程泄漏,同时记住for循环的对象如果是channel时只有当channel关闭才会结束
结论:
创建channel对象时要考虑何时关闭channel对象,比如上例子在函数末尾加上close(taskChan)即可解决协程泄漏问题。另外考虑如下对代码的改造:此时对channel的关闭是可有可无的
正确写法:
func bugfunc() { taskChan := make(chan int, 100) for i := 0; i < 100; i++ { taskChan <- i } consumer := func(d int) { fmt.Println(d) } for i := 0; i < 100; i++ { j <- taskChan go consumer(j) } close(taskChan) }
3 关注公众号
微信公众号:堆栈future