go语言channel
用于多个协程间安全的通信,关于chan的哲学:通信来共享内存而不要共享内存来通信。go语言底层会确保chan数据类型的同一块内存在同一时间内只被一个协程所操作,所以go语言通过通道实现多个协程间通信是安全的。
有缓冲chan异步无缓冲chan同步
通道chan有无缓冲不需要过多解释,写法如下:
ch1 := make(chan bool, 3) // 有缓冲 ch2 := make(chan bool) // 无缓冲
无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道则没有这种保证。
同步异步是相对于通道发送者和通道接受者而言:Talk is cheap. Show me the code.
1、无缓冲通道:发送者不发送接受者阻塞,接受者不接受发送者阻塞;
ch := make(chan bool) go func() { fmt.Println("send1") ch <- true // 无缓冲通道是空的,这个仍然能发出去 fmt.Println("send2") ch <- true // 无缓冲通道接受者未接收,发送者阻塞 fmt.Println("send3") ch <- true //close(ch) }() rev := <-ch fmt.Println(rev) time.Sleep(10 * time.Second) fmt.Println("finish") // 输出如下---------------- // // send1 // send2 // true // finish // // send1被接收使用, send2没有被接受,send3阻塞
2、有缓冲通道:通道未满发送者可发,通道已满发送者阻塞
ch := make(chan bool, 2) go func() { fmt.Println("send1") ch <- true fmt.Println("send2") ch <- true fmt.Println("send3") fmt.Println(len(ch)) ch <- true fmt.Println("send4") fmt.Println(len(ch)) // 通道缓冲已满下一个发送阻塞 ch <- true fmt.Println("send5") ch <- true //close(ch) }() fmt.Println(<-ch) time.Sleep(10 * time.Second) fmt.Println("finish") // 输出如下---------------- // // send1 // send2 // send3 // 1 // send4 // 2 // true
3、有缓冲通道:通道有数据可持续读通道为空依然阻塞
ch := make(chan bool, 2) go func() { fmt.Println("send1") ch <- true //close(ch) }() fmt.Println(<-ch) fmt.Println(<-ch) // 通道无数据阻塞 time.Sleep(10 * time.Second) fmt.Println("finish") // 输出如下后阻塞---------------- // // send1 // true
通道无数据时对接受者而言,有无缓冲都是阻塞。
因go官方对go1有向后兼容性保证,上述代码是在go1.16.8下测试,料想其他版本应该也是一致的。
nil的chan读写永久阻塞
什么是为nil
的chan?
map
、slice
、chan
是go语言里的3种引用类型,初始化时在make和new两个内建函数里只能使用make函数(这里并不是说这个三种类型只能使用make来初始化)。没有给一个chan分配内存时这个通道就是一个nil的chan,换句话说只是申明一个通道而没有初始化这个通道时就是一个nil的chan。
// 申明一个chan而没有初始化 // 这个是时候ch的值就是nil var ch chan bool
下面这段代码演示nil的chan阻塞:
var ch1 chan bool go func() { fmt.Println("here1") rev := <-ch1 // 阻塞 fmt.Println(rev) // 这段是不会打印出来的 }() fmt.Println("here2") ch1 <- false // 阻塞 fmt.Println(ch1) // 这段是不会打印出来的
关闭的chan发送数据引发panic
给一个已关闭的chan发送数据会引发panic:panic: send on closed channel
ch := make(chan bool) go func() { for { ch <- true // 不断的发,父协程在发的过程中把通道关了就会panic } }() rev := <-ch fmt.Println(rev) fmt.Println(<-ch) close(ch) fmt.Println("finish") time.Sleep(3 * time.Second) // 主协程运行太快可能还没输出panic就终止了,这儿人为暂停主协程3秒
关闭的chan读取数据:有值或零值
1、从关闭的无缓冲chan读数据:零值
从关闭的通道读取数据不会引发panic,这点儿与给关闭的通道发送数据是有本质区别的。
ch := make(chan bool) go func() { fmt.Println("send1") ch <- true close(ch) }() fmt.Println(<-ch) fmt.Println(<-ch) // 通道无数据获取零值即bool类型的零值false time.Sleep(10 * time.Second) fmt.Println("finish") // 输出如下后---------------- // // send1 // true // false // finish
2、从关闭的有缓冲通道读数据:通道不为空读取到发送的值
这很好理解,go的chan使用通信来共享内存,有缓冲的通道里面还有数据虽然被关闭但是依然要能读取出来。示例代码见下方小结3
3、从关闭的有缓冲通道读数据:通道为空读取到零值
ch := make(chan bool, 2) go func() { fmt.Println("send1") ch <- true fmt.Println("send2") ch <- true fmt.Println("send3") ch <- true close(ch) }() fmt.Println(<-ch) // 通道无数据时会阻塞,等待协程开始发数据后才开始执行,通道有数据true fmt.Println(<-ch) // 通道有数据true fmt.Println(<-ch) // 通道有数据true fmt.Println(<-ch) // 通道已被关闭,通道也无数据获取零值即bool类型的零值false time.Sleep(10 * time.Second) fmt.Println("finish") // 输出如下---------------- // // send1 // send2 // send3 // true // true // true // false // finish
看到关闭通道读写的特性就会得出一个显而易见的结论:代码中关闭通道的逻辑应该写在发送方,而且尽量做到发送通道消息的只有一个协程,否则可能导致panic。
go定时器
go语言time包里提供了两个与定时相关的结构体是利用通道的绝佳例子:
// The Timer type represents a single event. // When the Timer expires, the current time will be sent on C, // unless the Timer was created by AfterFunc. // A Timer must be created with NewTimer or AfterFunc. type Timer struct { C <-chan Time r runtimeTimer } // A Ticker holds a channel that delivers ``ticks'' of a clock // at intervals. type Ticker struct { C <-chan Time // The channel on which the ticks are delivered. r runtimeTimer }
两个结构体里都提供了一个可导出的只读通道C <-chan Time
。 还提供了Stop()
、Reset(d Duration)
两个相类似的方法,当然返回值有些许差异,功能差不多都是停止或重置定时时长。对于Timer
而言如果执行过了还可以重置倒计时时长后再次执行1次。
1、Timer
Timer
通过通过 func NewTimer(d Duration) *Timer
创建,到了指定的时间会执行一次且仅执行一次,当然,可以在执行完以后通过调用 timer.Reset() 让定时器再次工作,并可以更改时间间隔。 这里的执行一次即给上述通道C发送消息。
ch := make(chan int) timer := time.NewTimer(time.Second * 1) go func() { var x int for { select { case <-timer.C: x++ fmt.Printf("%d,%s\n", x, time.Now().Format("2006-01-02 15:04:05")) if x < 10 { timer.Reset(time.Second * 2) // timer 只能按时触发一次,可通过Reset()重置后继续触发。 } else { ch <- x } } } }() // 另起一个协程也获取这个定时通道,会发现多个子协程读取通道是竞争关系,谁读取到谁消费 go func() { for m := range timer.C { fmt.Println(m) timer.Reset(time.Second * 2) } }() <-ch
输出结果很有意思:
看起来两个协程是你一个我一个触发,千万不要被这种间隔输出假象所迷惑。多个协程竞争读取通道的顺序是不确定的。
2、Ticker
Ticker
会不断的按照设定的间隔时间触发,除非主动终止运行。这里的不断触发是指在指定的时间间隔里不断给上述通道发送消息。
ticker := time.NewTicker(time.Second * 1) ch := make(chan int) go func() { var x int for x < 10 { select { case <-ticker.C: // 接收通道消息 x++ fmt.Println(x) } } ticker.Stop() ch <- 0 }() <-ch // 通过通道阻塞,让子协程执行完毕 -- 子协程执行完毕后关闭阻塞通道主协程退出进程退出 // 上述代码会间隔1秒从1暑促到10后退出
当然go语言time包还提供了快捷函数如:func After(d Duration) <-chan Time
、AfterFunc(d Duration, f func()) *Timer
、func Tick(d Duration) <-chan Time
等方法,本质上就是Timer
和Ticker
结构体的调用封装不再赘述。
也就是说go的定时器本质上是利用通道机制,倒计时后发送消息或定时不断发送消息,对于业务代码需要倒计时或定时触发特性的接收这个通道即可达成目的,业务代码是通道的接受者,而发送者由go语言底层封装我们不需要关注如何实现,当然也可以查看源码了解其具体的机制。
---
参考资料:
① http://c.biancheng.net/view/100.html
② https://www.cnblogs.com/f-ck-need-u/p/9994508.html
哟嚯,本文评论功能关闭啦~