go routine 可以使用 channel 来进行通信,使用通信的手段来共享内存。下面是一个 channel 使用的小例子。
Go channel实现顺序无限输出123123123..._Schuyler_yuan的博客-优快云博客
使用形式有下面几种,
- 无缓冲 channel(同步)
- 有缓冲 channel(异步)
- 需要注意 channel 的关闭时机
- 单向 channel
- channel + select,实现多路复用
基本语法
make(chan Type) 等价于 make(chan Type,0)
make(chan Type, capacity)
ch <- value // 发送 value 到 ch
<- ch // 接收并将其丢弃
x := <- ch // 从 ch 中接收数据,并赋值给x
x, ok := <- ch // 功能同上,检查通道是否已关,是否为空
在 Go 语言中声明一个通道 Channel 非常简单,
ch := make(chan string) // 无缓冲的 channel
上面的语句中,chan 是一个关键字,表示是 channel 类型,注意 channel 是 Go 中的一个引用类型,通过 make 来创建,string 表示 channel 里的数据是 string 类型。
通过通道的声明可以看到,channel 是一个集合类型,定义好通道之后就可以使用了。一个通道的操作只有两种,发送和接收。
接收:获取 channel 中的值,操作符为 <-chan,可以理解为从 channel 中读;
发送:向 channel 中发送值,把值放在 channel 中,操作符为 chan<-,可以理解为写到channel。
eg. 无缓冲 channel(不带 buffer 的 channel,用于同步通信)
package main
import (
"fmt"
)
func main() {
ch := make(chan string)
go func() {
fmt.Println("This is a go routine")
ch <- "let us go"
}()
fmt.Println("This is a main routine")
v := <- ch
fmt.Println("Value:", v)
}
使用 channel 代替 sleep 函数,运行这个实例发现,最后并没有退出,也达到了sleep函数的效果,即有效的并发执行完成。这里实现了同步阻塞,如果main goroutine 先到,就会阻塞那等 sub goroutine 的值;sub goroutine 先到,也会阻塞那等 main goroutine 的值。
程序为什么能在新协程完成之前不退出?
答:新启动的协程,要向通道类型变量 ch 中发送值,main 协程中从变量 ch 中接收值,如果 ch 通道没有值,会阻塞等待,一直阻塞到 ch 通道里面有值可以接收为止。
注意,如果一个并发实体在读取一个永远没有数据放入的通道,或者把数据放入一个永远不会被读取的通道中,那么它将被永远阻塞,导致死锁发生。
fatal error: all goroutines are asleep - deadlock!
通道 channel 就像两个协程之间架设的管道,一个协程要向管道发送数据,另一个协程要从这个管道读取数据,有点类似队列。
上面的实例创建的是无缓冲 channel,容量是 0,不能存储任何数据,所以无缓冲 channel 只能控制同步和传输数据,数据并不能在 channel 中停留,无缓冲 channel 也叫同步 channel。无缓冲通道的发送和接收是同时进行的。
有缓冲的 channel 类似于可阻塞的队列,内部元素是先进先出的,make 函数的第二个参数可以指定通道的容量大小,可以创建一个有缓冲的通道,也叫异步 channel。
bufCh := make(chan int, 5) // 有缓冲的 channel
上面的语句创建了容量为5的通道,内部元素类型是 int,也就是说,这个通道里边最多可以放5个类型为 int 的元素,有缓冲 channel 的内部有一个缓冲队列。
- 发送操作是向队列的尾部插入元素,如果队列已满,则阻塞等待,直到另一个 goroutine 执行接收操作,释放队列的空间。
- 接收操作是从队列头部获取元素,并把它从队列中删除,如果队列为空,则阻塞等待,直到另一个 goroutine 执行发送操作插入新的元素。
可以获取 channel 容量和其中元素的个数,无缓冲channel其实是一个容量大小为0的通道。
eg. 有缓冲 channel(带 buffer 的 channel,用于异步通信)
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int,3)
fmt.Println("len(ch)",len(ch),",cap(ch)",cap(ch))
go func() {
defer fmt.Println("sub goroutine end")
for i := 0; i < 3; i++ {
ch <- i
fmt.Println("sub goroutine send:", i, "len(ch)=", len(ch), ",cap(ch)=", cap(ch))
}
}()
time.Sleep(2*time.Second)
//close(ch) // 如果 channel buffer 里边放了3个数,把 channel 关了,还能读到 buffer 中的数
for i := 0; i < 3; i++ { // 队列:先进先出,如果发送数>接收数,也会阻塞
num := <-ch
fmt.Println("val:",num)
}
// fmt.Println("val:", <-ch) // 如果接收数>发送数,会deadlock阻塞
fmt.Println("main end")
}
运行结果如下,
channel 可以执行关闭通道操作,
close(ch) // 关闭 channel
如果通道关闭了,就不能向通道发送数据,如果发送就会异常;但可以接收数据,如果通道中没有数据,接收到的数据就是零值。channel 关闭后,不能写,可以读。试图向已经关闭的 channel 发送数据,会导致 panic(panic: send on closed channel);关闭已经关闭的 channel 会致使 panic(panic: close of closed channel);关闭 nil channel 也会导致 panic。
建议不要在接收端关闭 channel,因为在接收端无法判断发送端是否还会向通道中发送值。(除非创建的 channel 没有发送端,只有接收端,1-0 模式,只是为了同步功能使用,可以在接收端接收之前进行关闭,此时读出的值是 0;否则也会报出 deadlock。)
一般来说,关闭 channel 的原则是,
不在接收端关闭(经常用于1-N,有缓冲 channel 只有一个发送端的情况,由发送端来关闭);
不要关闭有多个并发发送者的 channel(经常用于N-1,由接收端来通知)
单向 channel
限制一个 channel 只能接收不能发送,或者只能发送不能接收。单向通道声明很简单,只需要在声明的时候加上箭头操作符即可。
sendCh := make(chan<- int)
recvCh := make(<-chan int)
在函数或者方法中,使用单向通道较多,这样可以防止一些操作影响通道。
func doAction(ch chan<- int) {
// 只能使用ch变量进行发送操作,如果执行接收操作,程序将编译不通过。
}
select + channel,实现多路复用
在 Go 语言中,select 语句就是用来监听和 channel 有关的 IO 操作,当 IO 操作发生时,触发相应的 case 动作。有了 select 语句,可以实现 main 主协程与 goroutine 线程之间的互动。
select { // select 中的 case 是一个个可以操作的通道
case <-ch1:
// todo
case <-ch2:
// todo
default:
// todo
}
多路复用可以简单理解为,n个通道中任意一个通道有数据产生,select 都可以监听到,然后执行相应的分支,接收数据并处理。
注意,
- select 语句只能用于 channel 通道的 IO 操作,每个 case 都必须是一个通道;
- 如果不设置 default 条件,当没有 IO 操作发生时,select 语句就会一直阻塞;
- 如果这些 case 中有一个可以执行,那么 select 语句就会选择该 case 执行;
- 如果有一个或多个 IO 操作发生时,Go 运行时会随机选择一个 case 执行,但此时将无法保证执行顺序;
- 如果一个 select 没有任何 case 被执行,那么就会一直等待下去;
- 如果有超时条件语句,判断逻辑为如果在这个时间段内一直没有满足条件的case,则执行这个超时case。如果此段时间内出现了可操作的case,则直接执行这个case。一般用超时语句代替了default语句,保证程序不阻塞。
- 对于 case 语句,如果存在通道值为 nil 的读写操作,则该分支将被忽略,可以理解为相当于从 select 语句中删除了这个 case;
- 对于空的 select 语句,会引起死锁;
- 有时候我们会让 main 函数阻塞不退出,如 http 服务,我们会使用空的 select{} 来阻塞 main goroutine,这里要注意一定要有一直活动的 goroutine,否则会报 deadlock。大家还可以把 select{} 换成 for{} 试一下,打开系统管理器看下 CPU 的占用变化;
- 对于在 for 中的 select 语句,不能添加 default,否则会引起 cpu 占用过高的问题。
eg.
启动3个 goroutine 从不同数据源(rpc、web、redis)获取同一个数据属性,并把得到的结果发送到3个 channel 中,哪个先获取到,就使用哪个 channel 的结果。
分析:在这种情况下,如果尝试去获取第一个通道的结果,程序将会被阻塞,无法获取剩下两个通道的结果,无法判断哪个先获取到,这时可以使用 select 语句实现多路复用。
package main
import (
"fmt"
"time"
"math/rand"
)
func main() {
rpcCh := make(chan string)
webCh := make(chan string)
redisCh := make(chan string)
go func() {
rpcCh <- getSourceData("rpc")
}()
go func() {
webCh <- getSourceData("web")
}()
go func() {
redisCh <- getSourceData("redis")
}()
select {
case ch := <-rpcCh:
fmt.Println("using rpc:", ch)
case ch := <-webCh:
fmt.Println("using web:", ch)
case ch := <-redisCh:
fmt.Println("using redis:", ch)
}
}
func getSourceData(src string) string {
// 随机时间模拟数据获取操作所需的时间
rand.Seed(time.Now().UnixNano())
nTime := rand.Intn(5)
fmt.Println(src, " time:", nTime, "s")
time.Sleep(time.Duration(nTime) * time.Second)
return src
}
运行结果如下,最终的输出结果取决于执行最快的那个goroutine。
使用 channel 发送和接收数据,达到数据传递的目的,而不是通过修改同一个变量的方式。在数据流动传递的场景中,要优先使用通道,channel 内部实现使用了互斥锁来保证并发的安全,它是并发安全的,而且性能也不错。
channel 和共享内存有什么区别?一个内部实现有锁,一个得自加锁
假如程序里边嵌套了多个 channel,其中一个 channel 报错 panic,会影响其他 channel 吗?
抛出的错误,主进程会捕获吗,如果捕获,在哪捕获?
参考:
深度解密Go语言之channel底层实现_有时候需要些疯狂的人的博客-优快云博客_channel底层实现
Golang 高效实践之并发实践channel篇 - 我是码客 - 博客园
[Go语言] 操作channel时遇到panic怎么办?_孙飞 Sunface的博客-优快云博客
go中select语句_燕双鹰...的博客-优快云博客_go select
Have Fun