go chan
chan 类型
- <发送/写>型 chan<-
- <接收/读>型 <-chan
- 双向型 chan
chan 操作
- 构造/初始化 make()
- 关闭 close()
- 判等 ==
- <发送/写>数据 chan <- send_data
- <接收/读>数据 recv_data := <- chan
- chan 关闭或有数据,读操作不阻塞
- chan 未关闭且无数据,读操作阻塞
package main
import "fmt"
func main() {
//双向型 chan, 零值 nil
var ch chan int
//输入型 chan->
var ci chan<- int
//输出型 <-chan
var co <-chan int
//make()
cc := make(chan int)//无缓冲通道
cc = make(chan int, 10)//容量为10的通道
ch = cc
//双向型赋值给单向型正确
ci = ch
co = ch
//单向型赋值给双向型错误
//ch = ci //❌
//ch = co //❌
fmt.Println(ci, co, ch) //0xc00008c000 0xc00008c000 0xc00008c000
//可以赋值 -> 类型兼容 -> 可以判等
b1 := ch == cc
b2 := ci == nil
b3 := ch == ci
b4 := ch == co
//不可以赋值 -> 类型不兼容 -> 不可以判等
// b5 := ci == co //❌
fmt.Println(ci, co, cc, ch) //0xc00008c000 0xc00008c000 0xc00008c000 0xc00008c00
fmt.Println(b1, b2, b3, b4) //true false true true
//chan 发送/写
ch <- 1
//chan 接收/读
out := <-ch
fmt.Println(out) // 1
ch <- 1
//close(), chan 关闭
close(ch) //关闭后读操作不阻塞
//chan 关闭后,还有数据.读操作,返回 数据 和 true
out1, ok1 := <-ch
//chan 关闭后,没有数据.读操作,返回 零值 和 false
out2, ok2 := <-ch
fmt.Println(out1, ok1, out2, ok2) // 1 true 0 fals
ch = make(chan int, 0)
//chan 没关闭,无数据,读操作阻塞
out = <-ch
}
无缓冲通道(同步通道)
无缓冲通道上的发送/接收操作都会阻塞,直到另一个goroutine在对应通道上执行接收/发送操作,这是值传递完成。
使用无缓冲通道进行的通信会导致发送和接收goroutine同步化,因此无缓冲通道也叫作同步通道。
package main
import (
"fmt"
)
func main() {
defer func() {
fmt.Println("defer")
}()
done := make(chan string)
go func() {
str := "12312412412412"
fmt.Println("done")
done <- str
}()
s := <-done //等待goroutine发送
fmt.Println(s)
}
单向通道类型
- 双向通道转换为单向通道是允许的,反过来则不行
- 使用内置的close函数可以用来关闭通道,不能向一个已经关闭的通道发送数据,否则会宕机。
- 接收方可以通过
ret, ok := <- channel
通过bool值ok来判断通道是否关闭 - 可以通过
range
循环语法来接收通道上的消息
package main
import (
"fmt"
)
func counter(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}
func squareter(out chan<- int, in <-chan int) {
for x := range in {
out <- x * x
}
close(out)
}
func printer(in <-chan int) {
for v := range in {
fmt.Printf("%d ", v)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go squareter(ch2, ch1)
printer(ch2)
}
缓冲通道
在make(chan int, size)
时指定了通道的容量大小,即为缓冲通道。
- 通道为满时,发送操作所在的goroutine将会阻塞,直到接收方goroutine执行接收操作,使通道留出可写空间
- 通道为空时,接收操作所在的goroutine将会阻塞,知道发送方goroutine执行发送操作
- 通道和goroutine的调度深度关联,将缓冲通道作为队列在单个goroutine中使用是个错误的选择,如果没有另外一个goroutine从通道中接收,发送者有被永久阻塞的风险。如果仅需要一个简单队列,可以使用slice创建一个。
goroutine泄露
Go 中的并发性是以 goroutine(独立活动)和 channel(用于通信)的形式实现的。处理 goroutine 时,程序员需要小心翼翼地避免泄露。如果最终永远堵塞在 I/O 上(例如 channel 通信),或者陷入死循环,那么 goroutine 会发生泄露。即使是阻塞的 goroutine,也会消耗资源,因此,程序可能会使用比实际需要更多的内存,或者最终耗尽内存,从而导致崩溃。让我们来看看几个可能会发生泄露的例子。然后,我们将重点关注如何检测程序是否受到这种问题的影响。
回顾一下 goroutine 终止的场景:
-
当一个goroutine完成它的工作
-
由于发生了没有处理的错误
-
有其他的协程告诉它终止
goroutine泄露场景
- 1、发送不接收
如下所示,它只返回三个goroutine中第一个发送的消息
package main
import (
"fmt"
"math/rand"
"runtime"
"time"
)
func multiRequest() string {
response := make(chan string)
go func() {
time.Sleep(time.Duration(rand.Intn(4)+1) * time.Millisecond)
response <- "func1"
fmt.Println("go func1")
}()
go func() {
time.Sleep(time.Duration(rand.Intn(4)+1) * time.Millisecond)
response <- "func2"
fmt.Println("go func2")
}()
go func() {
time.Sleep(time.Duration(rand.Intn(4)+1) * time.Millisecond)
response <- "func3"
fmt.Println("go func3")
}()
return <-response
}
func main() {
defer func() {
fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
}()
rand.Seed(time.Now().Unix())
fmt.Println(multiRequest())
}
输出:
说明main在退出时,还有三个goroutine存在(包括main goroutine)
- 2、接收不发送
// leak 是一个有 bug 程序。它启动了一个 goroutine 阻塞接收 channel。当 Goroutine 正在等待时,leak 函数会结束返回。此时,程序的其他任何部分都不能通过 channel 发送数据,那个 channel 永远不会关闭,fmt.Println 调用永远不会发生, 那个 goroutine 会被永远锁死
func leak() {
ch := make(chan int)
go func() {
val := <-ch
fmt.Println("We received a value:", val)
}()
}
- 3、向已满的buffered channel发送,但是没有接收
和第一种情况比较类似。
在 channel 的接收值数量有限,且可以用 buffered channel 的情况下,那 buffer size 就分配的和接收值数量一样就可以解决这样的问题 - 4、select操作在所有case上阻塞
实现一个 fibonacci 数列生成器,并在独立的 goroutine 中运行,在读取完需要长度的数列后,如果 用于 退出生成器的 quit 忘了被 close (或写入数据),select 将一直被阻塞造成 该 goroutine 泄露
func fibonacci(c, quit chan int) {
x, y := 0, 1
for{
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go fibonacci(c, quit)
for i := 0; i < 10; i++{
fmt.Println(<- c)
}
// close(quit)
}
在这种需要一个独立的 goroutine 作为生成器的场景下,为了能在外部结束这个 goroutine,我们通常有两种方法:
1、使用上述实现里的模式,传入一个 quit channel,配合 select,当不需要的时候,close 这个 quit channel,该 goroutine 就可以退出。
2、使用 context 包
func fibonacci(c chan int, ctx context.Context) {
x, y := 0, 1
for{
select {
case c <- x:
x, y = y, x+y
case <-ctx.Done():
fmt.Println("quit")
return
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
c := make(chan int)
go fibonacci(c, ctx)
for i := 0; i < 10; i++{
fmt.Println(<- c)
}
cancel()
time.Sleep(5 * time.Second)
}
- 5、 goroutine进入死循环中,导致资源一直无法释放
goroutine 泄露的防范
- 创建goroutine时就要想好该goroutine该如何结束
- 使用chan时,要考虑到 chan阻塞时协程可能的行为
- 实现循环语句时注意循环的退出条件,避免死循环