简介-是什么、有什么用
channel字面意思是“通道”,用于goroutine之间进行通信、同步。
Goroutine 和 channel 是 Go 语言并发编程的两大基石。Goroutine用于执行并发任务,channel用于 goroutine之间的通信、同步。
看个简单demo:
package main
import (
"log"
"time"
)
var logger = log.Default()
func main() {
c1 := make(chan int)
go func() {
logger.Println("go sleep start")
time.Sleep(3 * time.Second)
logger.Println("go sleep end, send start")
c1 <- 1
logger.Println("send end")
}()
logger.Println("main receive start")
i, ok := <-c1
logger.Printf("main receive end, i:%d, ok:%t\n", i, ok)
}
运行程序可以看到main协程从通道读取值时会阻塞,直到另一个协程往通道发送数据,才会唤醒main协程。
CSP模型
与主流语言通过共享内存来进行并发控制方式不同,Go 语言采用了 CSP 模型。
共享内存存在竞态问题,需要加锁同步,会造成性能问题。
CSP (Communicating Sequential Process ),即通信顺序进程, 是一种并发编程模型,是一个很强大的并发数据模型,是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。
强调通信,有这么一句话:
不要通过共享内存来通信,而要通过通信来实现内存共享。
Go语言通过协程Goroutine和通道Channel实现了 CSP 模型。go语言并没有完全实现了CSP模型的所有理论,仅借用了 process和channel这两个概念(分别对应go语言的goroutine和channel):process是并发执行的实体,每个实体之间通过channel通讯来实现数据共享。
channel详解
channel具有如下特性:
- 并发安全
- 先进先出
- 能阻塞和唤醒goroutine
channel类型
channel是go语言的一种特殊类型,它是一个引用类型,因此未初始化时其默认零值是nil。
- 定义channel变量:
var 变量名称 chan 元素类型
- 元素类型,是指channel通道中元素的具体类型。
例子:
var c1 chan int
var c2 chan string
var c3 chan bool
var c4 chan []int
- 初始化channel,使用
make:
未初始化,默认零值是
nil,对nil通道发送或接收都会一直阻塞。
make(chan 元素类型, [缓冲区容量])
- 缓冲区容量:是可选参数,不指定或为0则创建无缓冲通道,指定且大于0则创建有缓冲通道。
例子:
c1 := make(chan int)
c2 := make(chan int, 1)
channel的操作
三种操作:
- 发送(即写操作):往通道中发送数据
- 接收(即读操作):从通道中读取数据
- 关闭
发送和接收使用<-符号。
发送
ch <- 10 // 把10发送到ch中
发送什么时候会阻塞?
当通道未关闭且缓冲区满时,发送数据就会阻塞,直到有其他协程消费数据使缓冲区不满才不再阻塞。
接收
x := <- ch // 接收并赋值
<-ch // 仅接收
接收什么时候会阻塞?
当通道未关闭且缓冲区空(无数据可读)时,接收数据就会阻塞,直到有其他协程向通道生产数据使缓冲区不空、有数据可读时才不再阻塞。
多返回值模式,接收操作可以使用多返回值模式:
value, ok := <- ch
- ok:表示是否成功从通道中读取数据。false,即未成功读取,表示通道为空、无数据可读了,且此时通道肯定关闭了,不然接收操作会阻塞。
注意:接收操作不阻塞,只有两种情况:1. 有数据可读、通道不空;2. 若无数据可读,则通道必须关闭了
- value:从通道中读取的值,若ok为false,则value是通道元素的数据类型的零值。
个人认为,多返回值模式的作用:如果ok返回false,则表示通道空了且已关闭。
nil通道
chan类型的默认零值是nil,对nil通道进行读、写都会阻塞。
关闭通道
使用close函数
close(ch)
注意:
- 通常由发送方执行关闭操作
-
关闭通道不是必须的(不像关闭文件是必须的),一般仅在接收方明确需要关闭信号时才执行关闭操作
关闭后的通道有如下特点:
- 不能再发送,再发送数据会panic
-
可以接收。通道被关闭,仍然可以读取未读完的数据;当通道为空(即没有数据了),再读取也不会造成阻塞,而是会返回对应数据类型的零值
重要:已关闭通道,一定不会阻塞读取操作
- 重复关闭会panic
close最佳实践:
- 非必要不要close
- 比较常见的是将close作为一种通知机制,通过close告诉消费者我关闭了,此时消费者消费就不会再阻塞了。
for range接收通道数据
for range可不断从通道中接收数据,当通道为空且已关闭了,会退出循环(因为为空且已关闭的通道是不可能再有数据了);当通道为空但未关闭,会阻塞直到通道有数据。
示例:
func main() {
c1 := make(chan int, 3)
go func() {
for i := 0; i < 10; i++ {
<-time.After(time.Second)
c1 <- i
}
<-time.After(5 * time.Second)
for i := 90; i < 100; i++ {
<-time.After(time.Second)
c1 <- i
}
<-time.After(5 * time.Second)
log.Default().Println("---close chan---")
close(c1)
}()
go func() {
for i := range c1 {
log.Default().Println(i)
}
log.Default().Println("---chan closed---")
}()
for {
}
}
单向通道
是什么?
只能发送或只能接收的通道就是单向通道。
为什么要单向通道?
某些场景下我们想限制使用者对通道的操作:只允许发送或接收,为了表明这种意图并防止被滥用,所以go语言提供了单向channel。
如何使用?
定义单向通道:
箭头<-和关键字chan的相对位置表明了当前通道允许的操作,这种限制将在编译阶段进行检测。
<- chan int // 只接收通道,只能接收不能发送
chan <- int // 只发送通道,只能发送不能接收
「只接收channel」不允许close,因为一般都是发送方来close通道。
通道类型转换:
双向通道可以转成单向通道(是隐式转换),但反之不行。
示例:
生产者只能向channel发送数据,消费者只能从channel接收数据,所以使用单向channel
func main() {
c1 := make(chan int, 1)
go producer(c1)
go consumer(c1)
for {
}
}
func producer(ch chan<- int) {
for i := 0; i < 10; i++ {
<-time.After(time.Second)
ch <- i
}
log.Default().Println("producer. close chan")
// 发送通道可close
close(ch)
}
func consumer(ch <-chan int) {
for i := range ch {
log.Default().Println("consume:", i)
// 接收通过不能close
//close(ch)
}
log.Default().Println("consumer. chan closed")
}
select多路复用
作用:
可以同时监听多个channel的发送或接收操作。
如何使用?
- 类似switch语句,select也有一系列case分支和一个default分支,每个case分支可以监听一个channel的发送或接收操作,若可接收或可发送,则匹配该case分支;没有任何case匹配,则走default分支。
- 若没有default分支,select会一直阻塞直到某个case匹配。
- 若没有case也没有default分支,select会一直阻塞。
- 若同时有多个case满足,select会随机选择一个case执行。
使用方式如下:
select {
case <-ch1:
//...
case data := <-ch2:
//...
case ch3 <- 10:
//...
default:
//默认操作
}
示例:
func main() {
ch := make(chan int, 1)
for i := 0; i < 10; i++ {
select {
case ch <- i:
log.Default().Println("send")
case x := <-ch:
log.Default().Println("receive, x:", x)
default:
log.Default().Println("default")
}
// 会panic panic: send on closed channel
//if i == 0 {
// close(ch)
//}
}
}
无缓冲channel和有缓冲channel
make(chan 元素类型, [缓冲区容量])
创建channel时:
- 未指定缓冲区容量或为0,则创建的是无缓冲channel
-
指定了缓冲区容量且大于0,则创建的是有缓冲channel
无缓冲channel
没有缓冲区,channel中不能缓存数据。
- 发送操作一定会阻塞,直到有人接收
-
接收操作一定会阻塞,直到有人发送
有缓冲channel
有缓冲区,channel中可以缓存数据。
- 仅当缓冲区满时,发送操作才阻塞,直到有人接收
-
仅当缓冲区空时,接收操作才阻塞,直到有人发送
len和cap方法
- len方法可以返回通道中元素的个数
- cap方法可以返回通道的缓冲区容量
对于「无缓冲channel」,len和cap都是0。
问题
channel有一个发送方,多个接收方怎么办?
多个接收方共同消费channel中的数据。
示例:
func main() {
ch1 := make(chan int, 1)
go consumer(ch1)
go consumer2(ch1)
go producer(ch1)
for {
}
}
func producer(ch chan<- int) {
for i := 0; i < 10; i++ {
<-time.After(time.Second)
ch <- i
}
log.Default().Println("producer. close chan")
// 发送通道可close
close(ch)
log.Default().Println("producer. end")
}
func consumer(ch <-chan int) {
for i := range ch {
log.Default().Println("consumer consume:", i)
}
log.Default().Println("consumer. chan closed")
}
func consumer2(ch <-chan int) {
for i := range ch {
log.Default().Println("consumer2 consume:", i)
}
log.Default().Println("consumer2. chan closed")
}
运行结果:
2022/10/30 01:46:46 consumer consume: 0
2022/10/30 01:46:47 consumer2 consume: 1
2022/10/30 01:46:48 consumer consume: 2
2022/10/30 01:46:49 consumer2 consume: 3
2022/10/30 01:46:50 consumer consume: 4
2022/10/30 01:46:51 consumer2 consume: 5
2022/10/30 01:46:52 consumer consume: 6
2022/10/30 01:46:53 consumer2 consume: 7
2022/10/30 01:46:54 consumer consume: 8
2022/10/30 01:46:55 consumer2 consume: 9
2022/10/30 01:46:55 producer. close chan
2022/10/30 01:46:55 producer. end
2022/10/30 01:46:55 consumer. chan closed
2022/10/30 01:46:55 consumer2. chan closed
goroutine泄漏
goroutine泄漏是什么?
是指goroutine一直阻塞而无法被回收。
不正确的使用channel可能导致goroutine泄漏,如下:
func main() {
ch := make(chan int)
go test(ch)
select {
case <-ch:
log.Default().Println("receive")
case <-time.After(time.Second):
log.Default().Println("time")
}
for {
}
}
func test(ch chan int) {
log.Default().Println("test run")
<-time.After(3 * time.Second)
ch <- 1
log.Default().Println("test end")
}
运行结果:
2022/10/30 01:55:17 test run
2022/10/30 01:55:18 time
发生了goroutine泄漏,
test方法的goroutine一直阻塞了,因为main gorutine无法接收通道
源码
待读