channel
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
channel类型
channel是一种类型,一种引用类型。声明通道类型的格式如下:
创建一个channel
通道是引用类型,通道类型的空值是nil
package main
import "fmt"
func main() {
//var 变量 chan 元素类型
var ch1 chan int
fmt.Printf("%T,%v\n", ch1, ch1)
ch2 := make(chan int, 3)
fmt.Printf("%T,%v\n", ch2, ch2)
}
channel操作
通道有发送(send)、接收(receive)和关闭(close)三种操作。
发送和接收都使用<-符号。
package main
import "fmt"
func main() {
ch := make(chan int, 1)
//发送
ch <- 10
// 接收
i, ok := <-ch
if !ok {
fmt.Printf("%v", ok)
return
}
fmt.Printf("%v", i)
// 关闭
close(ch)
}
// 对一个关闭的通道再发送值就会导致panic。
// 对一个关闭的通道进行接收会一直获取值直到通道为空。
// 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
// 关闭一个已经关闭的通道会导致panic。
无缓冲的通道
import "fmt"
func main() {
ch := make(chan int)
ch <- 10
c := <-ch
fmt.Println(c)
}
报错
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
D:/go/src/liqunkeji.com/dlq/main.go:7 +0x65
exit status 2
为什么会出现deadlock错误呢?
因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。
上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?
package main
import (
"fmt"
"sync"
)
var wy sync.WaitGroup
func chanan(c chan int) {
defer wy.Done()
ch := <-c
fmt.Println(ch)
}
func main() {
wy.Add(1)
ch := make(chan int)
go chanan(ch)
ch <- 10
wy.Wait()
}
无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。
使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。
有缓冲的通道
解决上面问题的方法还有一种就是使用有缓冲区的通道。我们可以在使用make函数初始化通道的时候为其指定通道的容量,例如:
package main
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 10
c := <-ch
fmt.Println(c)
}
只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。
我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量,虽然我们很少会这么做
for range 从通道循环取值
package main
import "fmt"
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for i := 0; i <= 99; i++ {
ch1 <- i
}
close(ch1)
}()
go func() {
for {
v, ok := <-ch1
if !ok {
break
}
ch2 <- v * v
}
close(ch2)
}()
for i := range ch2 {
fmt.Println(i)
}
}
从上面的例子中我们看到有两种方式在接收值的时候判断该通道是否被关闭,不过我们通常使用的是for range的方式。使用for range遍历通道,当通道被关闭的时候就会退出for range。
单向通道
有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。
Go语言中提供了单向通道来处理这种情况。例如,我们把上面的例子改造如下:
package main
import "fmt"
func count(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}
func squar(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func prin(out <-chan int) {
for i := range out {
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go count(ch1)
go squar(ch2, ch1)
prin(ch2)
}
其中,
chan<- int是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作;
<-chan int是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作。
在函数传参及任何赋值操作中可以将双向通道转换为单向通道,但反过来是不可以的。
通道总结
// channel nil 非空 空的 满的 没满
// 接收 阻塞 接收值 阻塞 接收值 接收值
// 发送 阻塞 发送值 发送值 阻塞 发送值
// 关闭 panic 关闭成功,读完数据后返回零值 关闭成功返回零值 关闭成功读取数据后返回零值 关闭成功读取数据后返回零值
关闭已经关闭的channel也会引发panic。