目录
- 一、channel
-
- 1 - channel简介
- 2 - channel的变量定义
- 二、channel同步
-
- 1 - 定义channel
- 2 - 无缓冲channel
- 3 - 有缓冲channel
- 4 - 关闭channel
- 5 - 单向channel
- 三、生产者消费者模型
一、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)
}