Go Channel 详解

 1.读写锁

 2.channel介绍

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

3.channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用<-符号。

现在我们先使用以下语句定义一个通道:

ch := make(chan int)

发送

将一个值发送到通道中。

ch <- 10 // 把10发送到ch中

接收

从一个通道中接收值。

x := <- ch // 从ch中接收值并赋值给变量x
<-ch       // 从ch中接收值,忽略结果

关闭

我们通过调用内置的close函数来关闭通道。

    close(ch)

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

    1.对一个关闭的通道再发送值就会导致panic。
    2.对一个关闭的通道进行接收会一直获取值直到通道为空。
    3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
    4.关闭一个已经关闭的通道会导致panic。

4.无缓冲的通道

 

无缓冲的通道又称为阻塞的通道。我们来看一下下面的代码:

func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Println("发送成功")
}

上面这段代码能够通过编译,但是执行的时候会出现以下错误:

    fatal error: all goroutines are asleep - deadlock!

    goroutine 1 [chan send]:
    main.main()
            .../src/github.com/pprof/studygo/day06/channel02/main.go:8 +0x54

为什么会出现deadlock错误呢?

因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。

上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?

一种方法是启用一个goroutine去接收值,例如:

func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}
func main() {
    ch := make(chan int)
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。

 

5.有缓冲的通道

解决上面问题的方法还有一种就是使用有缓冲区的通道。

我们可以在使用make函数初始化通道的时候为其指定通道的容量,例如:

func main() {
    ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做。

1.1.7. close()

可以通过内置的close()函数关闭channel(如果你的管道不往里存值或者取值的时候一定记得关闭管道)

package main

import "fmt"

func main() {
    c := make(chan int)
    go func() {
        for i := 0; i < 5; i++ {
            c <- i
        }
        close(c)
    }()
    for {
        if data, ok := <-c; ok {
            fmt.Println(data)
        } else {
            break
        }
    }
    fmt.Println("main结束")
}


6.如何优雅的从通道循环取值

当通过通道发送有限的数据时,我们可以通过close函数关闭通道来告知从该通道接收值的goroutine停止等待。当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。那如何判断一个通道是否被关闭了呢?

我们来看下面这个例子:

// channel 练习
func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    // 开启goroutine将0~100的数发送到ch1中
    go func() {
        for i := 0; i < 100; i++ {
            ch1 <- i
        }
        close(ch1)
    }()
    // 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
    go func() {
        for {
            i, ok := <-ch1 // 通道关闭后再取值ok=false
            if !ok {
                break
            }
            ch2 <- i * i
        }
        close(ch2)
    }()
    // 在主goroutine中从ch2中接收值打印
    for i := range ch2 { // 通道关闭后会退出for range循环
        fmt.Println(i)
    }
}

从上面的例子中我们看到有两种方式在接收值的时候判断通道是否被关闭,我们通常使用的是for range的方式。

 

7.单向通道

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

Go语言中提供了单向通道来处理这种情况。例如,我们把上面的例子改造如下:

func counter(out chan<- int) {
    for i := 0; i < 100; i++ {
        out <- i
    }
    close(out)
}

func squarer(out chan<- int, in <-chan int) {
    for i := range in {
        out <- i * i
    }
    close(out)
}
func printer(in <-chan int) {
    for i := range in {
        fmt.Println(i)
    }
}

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go counter(ch1)
    go squarer(ch2, ch1)
    printer(ch2)
}

其中,

    1.chan<- int是一个只能发送的通道,可以发送但是不能接收;
    2.<-chan int是一个只能接收的通道,可以接收但是不能发送。

在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的

 

 

8.通道总结

channel常见的异常总结,如下图:

通道总结

注意:关闭已经关闭的channel也会引发panic。

9.补充资料

https://golang.design/go-questions/channel/struct/

如何优雅关闭channel

https://golang.design/go-questions/channel/graceful-close/

 

使用 channel 的注意事项及死锁分析

未初始化的 channel 读写关闭操作

1.读:未初始化的channel,读取里面的数据时,会造成死锁deadlock

var ch chan int
<-ch  // 未初始化channel读数据会死锁

2.写:未初始化的channel,往里面写数据时,会造成死锁deadlock

var ch chan int
ch<-  // 未初始化channel写数据会死锁

3.关闭:未初始化的channel,关闭该channel时,会panic

var ch chan int
close(ch) // 关闭未初始化channel,触发panic

已初始化的 channel 读写关闭操作

1. 已初始化,没有缓冲区的channel

   // 代码片段1
   func main() {
        ch := make(chan int)
        ch <- 4
   }

代码片段1:没有缓冲channel,且只有写入没有读取,会产生死锁

 

   // 代码片段2
   func main() {
       ch := make(chan int)
       val, ok := <-ch
   }

代码片段2:没有缓冲channel,且只有读取没有写入,会产生死锁

 

   // 代码片段3
   func main() {
       ch := make(chan int)
       val, ok := <-ch
       if ok {
           fmt.Println(val)
       }
       ch <- 10 // 这里进行写入。但是前面已经产生死锁了
   }

代码片段3:没有缓冲channel,既有写入也有读出,但是在代码 val, ok := <-c 处已经产生死锁了。下面代码执行不到。

 

   // 代码片段4
   func main() {
   	ch := make(chan int)
   	ch <- 10
   	go readChan(ch)
   	
       time.Sleep(time.Second * 2)
   }
   
   func readChan(ch chan int) {
   	for {
   		val, ok := <-ch
   		fmt.Println("read ch: ", val)
   		if !ok {
   			break
   		}
   	}
   }

代码片段4:没有缓冲channel,既有写入也有读出,但是运行程序后,报错 fatal error: all goroutines are asleep - deadlock! 。

这是因为往 channle 里写入数据的代码 ch <- 10,这里写入数据时就已经产生死锁了。把 ch<-10 和 go readChan(ch) 调换位置,程序就能正常运行,不会产生死锁。

 

   // 代码片段5
   func main() {
   	ch := make(chan int)
   
   	go writeChan(ch)
   
   	for {
   		val, ok := <-ch
   		fmt.Println("read ch: ", val)
   		if !ok {
   			break
   		}
   	}
   
   	time.Sleep(time.Second)
       fmt.Println("end")
   }
   
   func writeChan(ch chan int) {
   	for i := 0; i < 4; i++ {
   		ch <- i
   	}
   }

代码片段5:没有缓冲的channel,既有写入,也有读出,与上面几个代码片段不同的是,写入channel的数据不是一个。

思考一下,这个程序会产生死锁吗?10 秒时间思考下,先不要看下面。



也会产生死锁,它会输出完数据后,报错 fatal error: all goroutines are asleep - deadlock!

为什么呢?这个程序片段,既有读也有写而且先开一个goroutine写数据,为什么会死锁?

原因在于 main() 里的 for 循环。可能你会问,不是有 break 跳出 for 循环吗?代码是写了,但是程序并没有执行到这里。

因为 for 会不停的循环,而 val, ok := <-ch, 这里 ok 值一直是 true,因为程序里并没有哪里关闭 channel 啊。你们可以打印这个 ok 值看一看是不是一直是 true。当 for 循环把 channel 里的值读取完了后,程序再次运行到 val, ok := <-ch 时,产生死锁,因为 channel 里没有数据了。

找到原因了,那解决办法也很简单,在 writeChan 函数里关闭 channel,加上代码 close(ch)。告诉 for 我写完了,关闭 channel 了。

加上关闭 channel 代码后运行程序:

read ch:  0 , ok:  true
read ch:  1 , ok:  true
read ch:  2 , ok:  true
read ch:  3 , ok:  true
read ch:  0 , ok:  false
end

程序正常输出结果。

对于没有缓冲区的 channel (unbuffered channel) 容易产生死锁的几个代码片段分析,总结下:

  1. channel 要用 make 进行初始化操作
  2. 读取和写入要配对出现,并且不能在同一个 goroutine 里
  3. 一定先用 go 起一个协程执行读取或写入操作
  4. 多次写入数据,for 读取数据时,写入者注意关闭 channel(代码片段5)

 

2. 已初始化,有缓冲区的 channel

// 代码片段1
func main() {
    ch := make(chan int, 1)
    val, ok := <-ch
}

代码片段1:有缓冲channel,先读数据,这里会一直阻塞,产生死锁。

 

   // 代码片段2
   func main() {
       ch := make(chan int, 1)
       ch <- 10
       ch <- 10
   }

代码片段2:同代码片段1,有缓冲channel,且 channel 缓冲只有容量 1(片段2示例),写多个值而没有读,也会阻塞,产生死锁。

   // 代码片段3
   func main() {
   	ch := make(chan int, 1)
   	ch <- 10
   
   	val, ok := <-ch
   	if ok {
   		fmt.Println(val, ok)
   	}
   }

代码片段3:有缓冲的channel,有读有写,正常的输出结果。

 

有缓冲区的channel总结:

    1. 如果 channel 满了,发送者会阻塞
    2. 如果 channle 空了,接收者会阻塞
    3. 如果在同一个 goroutine 里,写数据操作一定在读数据操作前
posted @ 2023-02-11 11:31  易先讯  阅读(216)  评论(0编辑  收藏  举报