Hi,我是行舟,今天和大家一起学习Go语言的Channel。
Go语言采用CSP模型,让两个独立执行的程序通过消息传递的方式共享内存,Channel就是Golang用来完成消息通讯的数据类型。
Go语言中,仍然可以使用共享内存的方式在多个协程间共享数据,只不过不推荐使用。
声明Channel
声明一个通道
var Channel类型 = chan 元素类型
除了上面的声明方式,还可以在chan的左右添加<-符号,分别表示只读通道和只写通道。
看几个实际的例子:
package main
import "fmt"
func main() {
var c1 chan int // 可读写的通道
var c2 chan<- float64 // 只写通道
var c3 <-chan int // 只读通道
fmt.Printf("c1=%+v n",c1)
fmt.Printf("c2=%+v n",c2)
fmt.Printf("c3=%+v n",c3)
}
只声明未初始化的通道值是nil,需要初始化之后才会分配存储空间,通道初始化使用make方法。make方法的第二个参数定义了通道可以缓冲参数的个数。
c1 := make(chan int) // 初始化无缓冲的通道
c2 := make(chan float64,10) // 初始化可以缓冲10个元素的通道
fmt.Printf("c1=%+v n",c1)
fmt.Printf("c2=%+v n",c2)
如上面代码c1这种没有缓冲空间的通道,我们称为无缓冲通道;c2称为有缓冲通道。
基本用法
写入和读取数据
我们执行下面的代码
c1 := make(chan int , 10) // 初始化可以缓冲10个元素的通道
c1 <-1
c1 <-2
初始化通道c1,并写入数据。
看下一个例子
c1 := make(chan int , 10) // 初始化可以缓冲10个元素的通道
c2 := make(chan float64) // 初始化无缓冲通道
c1 <- 1 // 往通道c1写值
c2 <- 1.01 // 往通道c2写值,会报错
此时将会看到这样的报错:
fatal error: all goroutines are asleep - deadlock!
这是因为我们往c2这个无缓冲通道中,写入数据,而c2没有读操作。我们加一行对c2的读操作
c2 := make(chan float64) // 初始化无缓冲通道
c1 <- 1 // 往通道c1写值
c2 <- 1.01 //往通道c2写值
<-c2
此时还是报和上面同样的错误。这是因为无缓冲通道的读写必须位于不同的协程中。
c1 := make(chan int , 10) // 初始化可以缓冲10个元素的通道
c2 := make(chan float64) // 初始化无缓冲通道
go func() {
fmt.Printf("c2=%+v n", <-c2) // 读取c2中的数据,输出c2=1.01
}()
c2 <- 1.01 // 往通道c2写值
c1 <- 1 // 往通道c1写值
time.Sleep(1*time.Second) // 短暂的sleep,等待协程读取channel数据
这样写,才是正确的方式,程序可以正常运行。
读取通道的数据时,通道左边如果是一个变量,会返回通道中的元素;如果是两个变量,第一个是通道中复制出来的元素,第二个是通道的状态。其中通道的状态为true时,通道未关闭,状态为fasle时,通道关闭。已经关闭的通道不允许再发送数据。
c1 := make(chan int , 10) // 初始化可以缓冲10个元素的通道
c1 <- 1 // 往通道c1写值
ret,status := <- c1
fmt.Printf("r=%+v,status=%+v",ret,status) // 输出 r=1,status=true
关闭通道
关闭通道的方法是close方法。
c := make(chan int ,5)
c<-1
close(c)
c<-2 // 往关闭的通道中发送数据会报错
调用close方法关闭c通道,然后继续往c通道发送数据会报错。
panic: send on closed channel
调用close方法关闭通道时,会给所有等待读取通道数据的协程发送消息。这是一个非常有用的特性。
c := make(chan int)
go func() {
ret,status := <-c
fmt.Printf("go rountine 1 ret=%+v,status=%+v n",ret,status)
}()
go func() {
ret,status := <-c
fmt.Printf("go rountine 2 ret=%+v,status=%+v n",ret,status)
}()
close(c)
time.Sleep(1*time.Second)
虽然通道可以关闭,但并不是一个必须执行的方法,因为通道本身会通过垃圾回收器,根据它是否可以访问来决定是否回收。
遍历通道
遍历通道内的所有数据
c := make(chan int, 5)
c <- 1
c <- 2
c <- 3
c <- 4
c <- 5
go func() {
for ret := range c{
fmt.Printf("ret=%d n",ret)
}
}()
time.Sleep(2*time.Second)
上文很多例子中都在示例的最后加了 time.Sleep(1*time.Second) ,让主程序等待1s钟之后再退出。因为main函数也是一个goroutine,它执行完成就会退出,而不会判断是否有其他协程需要执行。我们让main goroutine等待1s钟,给其他协程足够的执行时间。
select
select是Golang中的控制结构,和其它语言的switch语句写法类似。不过select的case语句必须是通道的读写操作。
如果有多个 case 都可以运行,select 会随机选出一个执行,其他case不会执行。default在没有case可 执行时,总可以执行。
如下示例
c1 := make(chan int ,10)
c2 := make(chan int ,10)
c3 := make(chan int ,10)
var i1, i2 int
c1 <- 10
c3 <- 20
select {
case i1 = <-c1:
fmt.Printf("received i1=%d n", i1) // 输出 received i1=10
case c2 <- i2:
fmt.Printf("sent %d n", i2 ) // 输出 sent 0
case i3, ok := (<-c3): // 等价于 i3, ok := <-c3
if ok {
fmt.Printf("received i3=%d n", i3) // 输出received i3=20
} else {
fmt.Printf("c3 is closedn")
}
default:
fmt.Printf("no communicationn")
}
我们多次运行这段代码会发现, 三个case都有可能执行到,这也验证了,select在满足多个case操作时,会在满足条件的case中随机选择一个执行。
当select语句没有case条件满足,且没有定义default语句时,当前select所在协程会陷入阻塞状态。
通过time.After( 1* time.Second),方法在1s之后会给通道发送消息,完成对select的超时操作:
c1 := make(chan int)
select{
case <- c1:
fmt.Println("print c1" )
case <-time.After( 1* time.Second):
fmt.Println("print 1s钟" )
}
select经常和for一起使用,下面是两者一起使用的一些例子:
c1 := make(chan int ,10)
// 把数组中的元素依次放入Channel
for _, str := rang []string{"a","b","c"} {
select{
case <- done:
return
case c1 <- str
}
}
done := make(chan int)
// 无限循环,直到满足某个条件,操作done通道,完成循环
for{
select{
case <- done:
return
default:
//进行某些操作
}
}
通道的特性
通过学习Golang语言源码和一些教程中了解到,通道有几个重要的特性,需要理解并牢记。
-
通道可以作为参数在函数中传递,当作参数传递时,复制的是引用。 -
通道是并发安全的。 -
同一个通道的发送操作之间是互斥的,必须一个执行完了再执行下一个。接收操作和发送操作一样。 -
缓冲通道的发送操作需要复制元素值,然后在通道内存放一个副本。非缓冲通道则直接复制元素值的副本到接收操作。 -
往通道内复制的元素如果是引用类型,则复制的是引用类型的地址。 -
缓冲通道中的值放满之后,再往通道内发送数据,操作会阻塞。当有值被取走之后,会优先通知最早被阻塞的goroutine,重新发送数据。如果缓冲通道中的值为空,再从缓冲通道中接收数据也会被阻塞,当有新的值到来时,会优先通知最早被堵塞的goroutine,再次执行接收操作。 -
非缓冲通道,无论读写,都是堵塞的,都需要找到配对的操作方才能执行。 -
对于刚初始化的nil通道,他的发送和接收操作会永远阻塞。
高级示例
我们使用Channel完成两个常见的问题,以加深对Channel的理解。第一个,借助通道,使两个协程交替输出大小写字母。
package main
import (
"fmt"
"time"
)
func main() {
arr1 := []string{"a","b","c","d","e"}
arr2 := []string{"A","B","C","D","E"}
a := make(chan bool)
b := make(chan bool)
go func() {
for _,str := range arr1{
if <-a {
fmt.Printf(str)
b <- true
}
}
}()
go func() {
for _,str := range arr2{
if <-b {
fmt.Printf(str)
a <- true
}
}
}()
a<-true
time.Sleep(2*time.Second)
}
我们定义了a,b两个channel,利用无缓冲通道接收堵塞的特性,在两个goroutine中,接收通道的值并作为继续执行的依据,从而达到交替执行的目的。
第二个,爬取指定的网站
package main
import (
"fmt"
"time"
)
// 抓取网页内容
func crawl(url string) (result string) {
time.Sleep(1*time.Second) // 睡眠1s钟模拟抓取完数据
return url+":抓取内容完成 n"
}
// 保存文件内容到本地
func saveFile(url string,limiter chan bool, exit chan bool) {
fmt.Printf("开启一个抓取协程 n")
result := crawl(url) // 抓取网页内容
if result != "" {
fmt.Printf(result)
}
<-limiter // 通知限速协程,抓取完成
if (exit != nil){
exit<-true // 通知退出协程,程序执行完成
}
}
// urls是要爬取的地址,n并发goroutine限制
func doWork(urls []string,n int) {
limiter := make(chan bool,n) // 限速协程
exit := make(chan bool) // 退出协程
for i,value := range urls{
limiter <- true
if i == len(urls)-1 {
go saveFile(value,limiter,exit)
}else{
go saveFile(value,limiter,nil)
}
}
<-exit
}
func main() {
urls := []string{"https://www.lixiang.com/","https://www.so.com","https://www.baidu.com/","https://www.360.com/"}
doWork(urls, 1)
}
我们通过limiter协程的缓冲区大小,控制协程并发数量。通过exit协程的阻塞,结束最终程序。
实现原理
Channel在Golang中用hchan结构体表示。
type hchan struct {
qcount uint // channel通道中元素个数
dataqsiz uint // 环形队列中数据大小
buf unsafe.Pointer // 存放实际元素的位置
elemsize uint16 // channnel类型大小
closed uint32 // channnel是否关闭
elemtype *_type // channel中元素类型
sendx uint // 发送的goroutine在buf中的位置
recvx uint // 接收的goroutine在buf中的位置
recvq waitq // 等待读取的goroutine队列
sendq waitq // 等待写入的goroutine队列
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
lock mutex // channel并发锁
}
buf中存放了所有缓冲的数据,结合sendx,recvx,构造了一个环形队列结构。
通道初始化时,根据元素大小、是否含有指针决定存储空间的分配。当元素大小为0时,只分配hchan结构体的内存就可以了。当没有指针时,连续分配元素大小和结构体大小的内存。当存在指针时,需要给指针元素单独分配内存空间。
通道写入数据的过程中,首先判断是否有正在等待读取的协程,如果有的话,复制数据给此协程;否则继续判断是否有空闲缓冲区,如果有的话把数据复制到缓冲区;否则,把当前goroutine放入等待写入队列。
通道读取数据的流程和写入类似,首先判断是否有等待写入的协程,如果有的话,启动协程的写入操作,复制数据;否则继续判断缓冲区中是否有数据,如果有的话复制数据;否则,把当前goroutine放入等待读取的队列
Go Channel的源码,主要在runtime/chan.go目录下。
总结
本文主要介绍了Go Channel的基本用法,特性,常用场景和实现原理。
原文始发于微信公众号(一行舟):Go Channel(收藏以备面试)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/20305.html