channel是Go语言中的一个核心类型,可以把它看成管道。编程难度肯定是有一些的,哈哈。
“Go 强调不要通过共享内存来通讯,而是通过通讯来共享内存。”
很多人都说上面的话,具体怎么理解呢?
chan是一种引用类型,引用类型 channel可用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。
举个例子:
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
fmt.Println("input", i)
}
}()
for i := 0; i < 5; i++ {
num := <-ch
fmt.Println("output", num)
}
}
打印结果:
$ go run main.go
input 0
output 0
output 1
input 1
input 2
output 2
output 3
input 3
input 4
output 4
可以看到input output都是成对出现的,因为无缓冲chan,当只有读,没有写时,“读”阻塞。当只有写,没有读,那么 “写”阻塞。
- 接下来是个反例:
package main
func main(){
ch:=make(chan int)
ch <- 1
go func (){
<-ch
}()
}
这个就会卡死,因为创建goroutine在chan的写之后,永远不会执行读。
- 另一个反例:
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("log:", doReq(3*time.Second))
}
func doReq(timeout time.Duration) (res int) {
ch := make(chan int)
go func() {
res := do()
ch <- res
}()
select {
case res = <-ch:
return res
case <-time.After(timeout):
return 0
}
}
func do() int {
time.Sleep(4 * time.Second)
return 111
}
我们创建了无缓冲ch,goroutine 执行 do 函数并通过ch 将结果发送回main,这是goroutine阻塞,直到main从 ch 接收到结果为止。同时main将在 select 阻塞,直到goroutine 将结果发送给 ch或超时3秒。如果超时先发生,则main将return 0,这会导致goroutine 写 ch 数据一直堵塞。这样内存就泄露啦!
- 最后一个反例:
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
var rw sync.RWMutex
func write(a int, out chan<- int) {
for {
a++
rw.Lock()
out <- a
fmt.Printf("goroutine 写%d\n", a)
rw.Unlock()
}
}
func read(in <-chan int) {
for {
time.Sleep(time.Millisecond * 500)
rw.RLock()
a := <-in
fmt.Printf("goroutine 读 %d\n", a)
rw.RUnlock()
}
}
func main() {
ch := make(chan int, 5)
for i := 0; i < 100; i++ {
go read(ch)
}
for i := 0; i < 100; i++ {
go write(0, ch)
}
for {
runtime.GC()
}
}
这种就是锁跟chan相互抢占cpu的反例,因为写的时候会不断的上写锁,读的时候拿不到读锁,也就没办法读chan,连带着写chan也会因为达到limit而阻塞,最后写锁也不会被释放。最后就死锁了。