Channel就是Golang用来完成消息通讯的数据类型。Go语言中,仍然可以使用共享内存的方式在多个协程间共享数据,只不过不推荐使用。
声明Channel
在chan的左右添加<-符号,分别表示只读通道和只写通道。
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)
/*
c1=(chan int)(nil)
c2=(chan<- float64)(nil)
c3=(<-chan int)(nil)
*/
从上面可以看到只声明未初始化的通道值是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=(chan int)(0xc00006a120)
c2=(chan float64)(0xc000120000)
*/
如上面代码c1这种没有缓冲空间的通道,我们称为非缓冲通道;非缓冲通道,无论读写,都是堵塞的,都需要找到配对的操作方才能执行。c2称为有缓冲通道。
基本用法
写入和读取数据
case1:
初始化通道c1,并写入数据。
package main
func main() {
c1 := make(chan int,10)// 初始化可以缓冲10个元素的通道
c1 <- 10
}
case2:
我们往c1这个无缓冲通道中,写入数据,而c1没有读操作。报错
c1 := make(chan int) // 初始化无缓冲通道
c1 <- 10 // 往通道c1写值 报错
/*
fatal error: all goroutines are asleep - deadlock!
*/
case3:
报错!!!注意无缓冲通道的读写必须位于不同的协程中。
c1 := make(chan int) // 初始化无缓冲通道
c1 <- 10 // 往通道c1写值 报错
<-c1
/*
fatal error: all goroutines are asleep - deadlock!
*/
case4:
正确使用无缓冲通道方式
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan int) // 初始化可以缓冲10个元素的通道
go func() {
fmt.Printf("c1=%#v \n", <-c1) // 读取c1中的数据,输出c1=10
}()
c1 <- 10 // 往通道c1写值
time.Sleep(1*time.Second) //等待协程读取channel数据
}
/*
c1=10
*/
case5:
读取通道的数据时,通道左边如果是一个变量,会返回通道中的元素; 如果是两个变量,第一个是通道中复制出来的元素,第二个是通道的状态。其中通道的状态为true时,通道未关闭,状态为false时,通道关闭。已经关闭的通道不允许再发送数据。
c1 := make(chan int , 10) // 初始化可以缓冲10个元素的通道
c1 <- 10 // 往通道c1写值
v,status := <- c1
fmt.Printf("v=%#v,status=%#v",v,status)
/*
v=10,status=true
*/
关闭通道
case1:
关闭通道的方法是close方法。调用close方法关闭c通道,然后继续往c通道发送数据会报错。
c := make(chan int ,5)
c<-10
close(c)
c<-5 // 往关闭的通道中发送数据会报错
/*
panic: send on closed channel
*/
case2:
调用close方法关闭通道时,会给所有等待读取通道数据的协程发送消息。这是一个非常有用的特性。
c := make(chan int)
go func() {
ret,status := <-c
fmt.Printf("rountine 1 v=%#v,status=%#v \n",ret,status)
}()
go func() {
ret,status := <-c
fmt.Printf("rountine 2 v=%#v,status=%#v \n",ret,status)
}()
close(c)
time.Sleep(1*time.Second)
/*
rountine 2 v=0,status=false
rountine 1 v=0,status=false
*/
虽然通道可以关闭,但并不是一个必须执行的方法,因为通道本身会通过垃圾回收器,根据它是否可以访问来决定是否回收。
遍历通道
遍历通道内的所有数据
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 5)
for i := 0; i < 5; i++ {
c <- i
}
go func() {
for v := range c {
fmt.Printf("v=%d \n", v)
}
}()
time.Sleep(time.Second)
}
select
select是Golang中的控制结构,和其它语言的switch语句写法类似。不过select的case语句必须是通道的读写操作。
如果有多个 case 都可以运行,select 会随机选出一个执行,其他case不会执行。default在没有case可 执行时,总可以执行。
package main
import (
"fmt"
)
func main() {
c1 := make(chan int ,10)
c2 := make(chan int ,10)
c3 := make(chan int ,10)
var i1, i2 ,i3 int
var ok bool
c1 <- 10
c3 <- 20
for{
select {
case i1 = <-c1:
fmt.Printf("received i1=%d \n", i1)
case c2 <- i2:
fmt.Printf("sent %d \n", i2 )
case i3, ok = <-c3:
if ok {
fmt.Printf("received i3=%d \n", i3)
} else {
fmt.Printf("c3 is closed\n")
}
default:
fmt.Printf("no communication\n")
return
}
}
}
/*
sent 0
sent 0
received i3=20
received i1=10
sent 0
sent 0
sent 0
sent 0
sent 0
sent 0
sent 0
sent 0
no communication
进程 已完成,退出代码为 0
*/
当select语句没有case条件满足,且没有定义default语句时,当前select所在协程会陷入阻塞状态。
通过time.After( time.Second),方法在1s之后会给通道发送消息,完成对select的超时操作:
c1 := make(chan int)
select{
case <- c1:
fmt.Println("c1" )
case <-time.After( 1* time.Second):
fmt.Println("1s" )
}
select经常和for一起使用
done := make(chan int)
// 无限循环,直到满足某个条件,操作done通道,完成循环
for{
select{
case <- done:
return
default:
//进行某些操作
}
}
通道的特性
- 通道可以作为参数在函数中传递,当作参数传递时,复制的是引用。
- 通道是***并发安全***的。
- 同一个通道的发送操作之间是互斥的,必须一个执行完了再执行下一个。接收操作和发送操作一样。
- 缓冲通道的发送操作需要复制元素值,然后在通道内存放一个副本。非缓冲通道则直接复制元素值的副本到接收操作。
- 往通道内复制的元素如果是引用类型,则复制的是引用类型的地址。
- 缓冲通道中的值放满之后,再往通道内发送数据,操作会阻塞。当有值被取走之后,会优先通知最早被阻塞的goroutine,重新发送数据。如果缓冲通道中的值为空,再从缓冲通道中接收数据也会被阻塞,当有新的值到来时,会优先通知最早被堵塞的goroutine,再次执行接收操作。
- 非缓冲通道,无论读写,都是堵塞的,都需要找到配对的操作方才能执行。
- 对于nil通道,他的发送和接收操作会永远阻塞。
高级示例
模拟爬虫
// 抓取网页内容
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 // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// 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
}
buf
是有缓冲的channel所特有的结构,用来存储缓存数据。是个循环链表sendx
和recvx
用于记录buf
这个循环链表中的发送或者接收的indexlock
是个互斥锁。recvq
和sendq
分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表
缓存中按链表顺序存放,取数据的时候按链表顺序读取,符合FIFO的原则。
send/recv
注意:缓存链表每一步的操作,都需要加锁
每一步的操作可以细化为:
- 第一,加锁
- 第二,把数据从goroutine中copy到“队列”中(或者从队列中copy到goroutine中)。
- 第三,释放锁
阻塞
Goroutine的并发编程模型基于GMP模型,简要解释一下GMP的含义:
**G:**表示goroutine,每个goroutine都有自己的栈空间,定时器,初始化的栈空间在2k左右,空间会随着需求增长。
**M:**抽象化代表内核线程,记录内核线程栈信息,当goroutine调度到线程时,使用该goroutine自己的栈信息。
**P:**代表调度器,负责调度goroutine,维护一个本地goroutine队列,M从P上获得goroutine并执行,同时还负责部分内存的管理。
Go的goroutine是用户态的线程,用户态的线程是需要自己去调度的,Go有运行时scheduler去完成调度。
goroutine的阻塞操作,实际上是调用send (ch <- xx)
或者recv ( <-ch)
的时候主动触发的。
看下面样例:
goroutine1 中
c := make(chan int,2)
c<-1
c<-2
// 已满
// 再放,阻塞
c<-3
这个时候goroutine1会主动调用Go的调度器,让goroutine1等待,并从让出M,让其他G去使用。
同时goroutine1也会被抽象成含有goroutine1指针和send元素的sudog
结构体保存到hchan的sendq
中等待被唤醒。
goroutine2 中
v:= <-c
goroutine2从缓存队列中取出数据,channel会将等待队列中的goroutine1推出,将goroutine1当时send的数据推到缓存中,然后调用Go的scheduler,唤醒goroutine2,并把goroutine2放到可运行的Goroutine队列中。
假如是先进行执行recv操作的G2会怎么样,同理倒推即可