一、channel
1 - channel简介
什么是channel :channel是Go语言中的一个核心类型,可以把它看成管道(FIFO)。并发核心单元通过它就可以发送或者接收数据进行通讯,这在一定程度上又进一步降低了编程的难度channel的作用
channel是一个数据类型,主要用来解决协程的同步问题以及协程之间数据共享(数据传递)的问题 引用类型 channel可用于多个 goroutine 通讯。其内部实现了同步,确保并发安全 goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine 奉行通过通信来共享内存 ,而不是共享内存来通信
2 - channel的变量定义
定义channel变量 : make(chan Type) //等价于make(chan Type, 0)
、make(chan Type, capacity)
和map类似,channel也一个对应make创建的底层数据结构的引用 当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用 ,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil 定义一个channel时,也需要定义发送到channel的值的类型。channel可以使用内置的make()函数来创建 chan是创建channel所需使用的关键字。Type 代表指定channel收发数据的类型 capacity参数
当参数capacity= 0 时,channel 是无缓冲阻塞读写的; 当capacity > 0 时,channel 有缓冲、是非阻塞的,直到写满 capacity个元素才阻塞写入 channel接收和发送数据 :
channel非常像生活中的管道,一边可以存放东西,另一边可以取出东西。channel通过操作符 <- 来接收和发送数据,发送和接收数据语法
channel <- value //发送value到channel
<-channel //接收并将其丢弃
x := <-channel //从channel中接收数据,并赋值给x
x, ok := <-channel //功能同上,同时检查通道是否已关闭或者是否为空
默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得goroutine同步变的更加的简单,而不需要显式的lock channel有两个端
一端:写端(传入端) chan <- 另一端: 读端(传出端)<- chan 要求:读端和写端必须同时满足条件(读端有数据可读,写端有写入数据),才在chan上进行数据流动。否则,则阻塞
package main
import (
"fmt"
"time"
)
var channel = make ( chan int )
func printer ( s string ) {
for _ , ch := range s {
fmt. Printf ( "%c" , ch)
time. Sleep ( 3000 * time. Millisecond)
}
}
func person1 ( ) {
printer ( "hello" )
channel <- 8
}
func person2 ( ) {
<- channel
printer ( "world" )
}
func main ( ) {
go person1 ( )
go person2 ( )
for {
}
}
二、channel同步
1 - 定义channel
定义channel :make(chan 类型,容量) ch := make (chan string)
写端:ch <- “hehe”;写端写数据,同时没有读端在读。写端阻塞 读端:str := <- ch;读端读数据, 同时没有写端在写,读端阻塞 len(ch):channel 中剩余未读取数据个数 cap(ch):通道的容量
package main
import "fmt"
func main ( ) {
ch := make ( chan string )
fmt. Println ( "len(ch)=" , len ( ch) , "cap(ch)=" , cap ( ch) )
go func ( ) {
for i := 0 ; i < 2 ; i++ {
fmt. Println ( "i = " , i, "len(ch)=" , len ( ch) , "cap(ch)=" , cap ( ch) )
}
ch <- "子go打印完毕"
} ( )
str := <- ch
fmt. Println ( "str=" , str)
}
2 - 无缓冲channel
无缓冲的通道(unbuffered channel) :是指在接收前没有能力保存任何值的通道,通道容量为0
这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。否则,通道会导致先执行发送或接收操作的 goroutine 阻塞等待 这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在 阻塞 :由于某种原因数据没有到达,当前协程(线程)持续处于等待状态,直到条件满足,才解除阻塞同步 :在两个或多个协程(线程)间,保持数据内容一致性的机制无缓冲的channel创建格式 :make(chan Type) //等价于make(chan Type, 0)
;如果没有指定缓冲区容量,那么该通道就是同步的,因此会阻塞到发送者准备好发送和接收者准备好接收图示两个 goroutine 如何利用无缓冲的通道来共享一个值
①.在第 1 步,两个 goroutine 都到达通道,但哪个都没有开始执行发送或者接收 ②.在第 2 步,左侧的 goroutine 将它的手伸进了通道,这模拟了向通道发送数据的行为。这时,这个 goroutine 会在通道中被锁住,直到交换完成 ③.在第 3 步,右侧的 goroutine 将它的手放入通道,这模拟了从通道里接收数据。这个 goroutine 一样也会在通道中被锁住,直到交换完成 ④.在第 4 步和第 5 步,进行交换,并最终,在第 6 步,两个 goroutine 都将它们的手从通道里拿出来,这模拟了被锁住的 goroutine 得到释放。两个 goroutine 现在都可以去做别的事情了
package main
import (
"fmt"
)
func main ( ) {
ch := make ( chan int )
go func ( ) {
for i := 0 ; i < 5 ; i++ {
fmt. Println ( "子go程, i=" , i)
ch <- i
}
} ( )
for i := 0 ; i < 5 ; i++ {
num := <- ch
fmt. Println ( "主go程读:" , num)
}
}
3 - 有缓冲channel
有缓冲channel创建 :ch := make(chan int, 5)
通道容量为非0
len(ch) : channel 中剩余未读取数据个数。 cap(ch): 通道的容量 如果给定了一个缓冲区容量,通道就是异步的。只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞地进行 有缓冲的通道 :(buffered channel)是一种在被接收前能存储一个或者多个数据值的通道
这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也不同 只有通道中没有要接收的值时,接收动作才会阻塞 只有通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞 channel 应用于 两个go程中;一个读,另一个写 缓冲区可以进行数据存储;存储至容量上限,阻塞;具备异步能力,不需同时操作channel缓冲区(发短信) 有缓冲和无缓冲通道的不同 :无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证
无缓冲channel具备同步能力(打电话) 有缓冲channel具备异步能力(发短信) 图示有缓冲通道在goroutine之间同步数据
①.在第 1 步,右侧的 goroutine 正在从通道接收一个值 ②.在第 2 步,右侧的这个 goroutine独立完成了接收值的动作,而左侧的 goroutine 正在发送一个新值到通道里 ③.在第 3 步,左侧的goroutine 还在向通道发送新值,而右侧的 goroutine 正在从通道接收另外一个值。这个步骤里的两个操作既不是同步的,也不会互相阻塞 ④.最后,在第 4 步,所有的发送和接收都完成,而通道里还有几个值,也有一些空间可以存更多的值
package main
import (
"fmt"
"time"
)
func main ( ) {
ch := make ( chan int , 3 )
fmt. Println ( "len=" , len ( ch) , "cap=" , cap ( ch) )
go func ( ) {
for i := 0 ; i < 8 ; i++ {
ch <- i
fmt. Println ( "子go程:i" , i, "len=" , len ( ch) , "cap=" , cap ( ch) )
}
} ( )
time. Sleep ( time. Second * 3 )
for i := 0 ; i < 8 ; i++ {
num := <- ch
fmt. Println ( "主go程读到:" , num)
}
}
4 - 关闭channel
何时需要关闭channel :如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现关闭channel注意事项
channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel 关闭channel后,无法向channel 再发送数据(引发 panic 错误后导致接收立即返回零值) 关闭channel后,可以继续从channel接收数据 对于nil channel,无论收发都会被阻塞 判断对端channel是否关闭 :if num, ok := <-ch ; ok == true {
如果对端已经关闭, ok --> false . num无数据 如果对端没有关闭, ok --> true . num保存读到的数据 写端已经关闭channel,可以从中读取数据
读无缓冲channel:读到0,说明写端关闭 读有缓冲channel:如果缓冲区内有数据,先读数据。读完数据后,可以继续读,读到0
package main
import (
"fmt"
)
func main ( ) {
ch := make ( chan int )
go func ( ) {
for i := 0 ; i < 8 ; i++ {
ch <- i
}
close ( ch)
} ( )
for {
if num, ok := <- ch; ok == true {
fmt. Println ( "读到数据:" , num)
} else {
n := <- ch
fmt. Println ( "关闭后:" , n)
break
}
}
}
可以使用 range 替代 ok :for num := range ch { // ch 不能替换为 <-ch
func main ( ) {
ch := make ( chan int , 0 )
go func ( ) {
for i := 0 ; i < 5 ; i++ {
ch <- i
}
close ( ch)
fmt. Println ( "子go 结束" )
} ( )
time. Sleep ( time. Second * 2 )
for num := range ch {
fmt. Println ( "读到数据:" , num)
}
}
5 - 单向channel
默认的channel 是双向的 :var ch chan int ch = make(chan int)
单向写channel :不能读操作;var sendCh chan <- int sendCh = make(chan <- int)
单向读channel :不能写操作;var recvCh <- chan int recvCh = make(<-chan int)
单向channel与双向channel转换
双向channel可以隐式转换为任意一种单向channel:sendCh = ch
单向channel不能转换为双向channel:ch = sendCh/recvCh error!!!
package main
import (
"fmt"
)
func main ( ) {
ch := make ( chan int )
var sendCh chan <- int = ch
sendCh <- 789
var recvCh <- chan int = ch
num := <- recvCh
fmt. Println ( "num=" , num)
}
func send ( out chan <- int ) {
out <- 89
close ( out)
}
func recv ( in <- chan int ) {
n := <- in
fmt. Println ( "读到" , n)
}
func main ( ) {
ch := make ( chan int )
go func ( ) {
send ( ch)
} ( )
recv ( ch)
}
三、生产者消费者模型
生产者消费者模型概念 :
某个模块(函数等)负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、协程、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者 单单抽象出生产者和消费者,还够不上是生产者/消费者模型。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据 生产者相当于发送数据端;消费者相当于接收数据端;channel相当于缓冲区 缓冲区的作用
①.解耦 ( 降低生产者 和 消费者之间 耦合度 ) ②.并发 (生产者消费者数量不对等时,能保持正常通信) ③.缓存 (生产者和消费者 数据处理速度不一致时, 暂存数据)
package main
import (
"fmt"
"time"
)
func producer ( out chan <- int ) {
for i := 0 ; i < 10 ; i++ {
fmt. Println ( "生产:" , i* i)
out <- i * i
}
close ( out)
}
func consumer ( in <- chan int ) {
for num := range in {
fmt. Println ( "消费者拿到:" , num)
time. Sleep ( time. Second)
}
}
func main ( ) {
ch := make ( chan int , 6 )
go producer ( ch)
consumer ( ch)
}
实际开发应用 :
在实际的开发中,生产者消费者模式应用也非常的广泛,例如:在电商网站中,订单处理,就是非常典型的生产者消费者模式 当很多用户单击下订单按钮后,订单生产的数据全部放到缓冲区(队列)中,然后消费者将队列中的数据取出来发送者仓库管理等系统 通过生产者消费者模式,将订单系统与仓库管理系统隔离开,且用户可以随时下单(生产数据)。如果订单系统直接调用仓库系统,那么用户单击下订单按钮后,要等到仓库系统的结果返回。这样速度会很慢
package main
import "fmt"
type OrderInfo struct {
id int
}
func producer2 ( out chan <- OrderInfo) {
for i := 0 ; i < 10 ; i++ {
order := OrderInfo{ id: i + 1 }
out <- order
}
close ( out)
}
func consumer2 ( in <- chan OrderInfo) {
for order := range in {
fmt. Println ( "订单id为:" , order. id)
}
}
func main ( ) {
ch := make ( chan OrderInfo)
go producer2 ( ch)
consumer2 ( ch)
}