4-Go协程间通信与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"
)

// 全局定义channel, 用来完成数据同步
var channel = make(chan int)

// 定义一台打印机
func printer(s string) {
	for _, ch := range s {
		fmt.Printf("%c", ch) // 共享资源 -> 屏幕:stdout
		time.Sleep(3000 * time.Millisecond)
	}
}

// 定义两个人使用打印机
func person1() {
	printer("hello")
	channel <- 8
}
func person2() {
	<-channel // 阻塞,直到person1执行完“channel <- 8”的写入操作后,才继续执行
	printer("world")
}

func main() {
	go person1()
	go person2()
	for {

	}
}

二、channel同步

1 - 定义channel

  • 定义channelmake(chan 类型,容量) ch := make (chan string)
    • 写端:ch <- “hehe”;写端写数据,同时没有读端在读。写端阻塞
    • 读端:str := <- ch;读端读数据, 同时没有写端在写,读端阻塞
    • len(ch):channel 中剩余未读取数据个数
    • cap(ch):通道的容量
package main

import "fmt"

func main() {
	ch := make(chan string) // 无缓冲channel
	// len(ch) : channel 中剩余未读取数据个数。 cap(ch): 通道的容量。
	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))
		}
		// 通知主go打印完毕
		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 // ch <- 0
		}
	}()
	//time.Sleep(time.Second * 2)
	for i := 0; i < 5; i++ {
		num := <-ch
		fmt.Println("主go程读:", num)
	}
}

/* 调试输出
	子go程, i= 0
	子go程, i= 1
	主go程读: 0  //因为主go程的IO操作耗时导致这时候才输出
	主go程读: 1
	子go程, i= 2
	子go程, i= 3
	主go程读: 2
	主go程读: 3
	子go程, i= 4
	主go程读: 4
*/

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) // 存满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)
	}
}

/* 调试输出
	
	len= 0 cap= 3
	子go程:i 0 len= 1 cap= 3
	子go程:i 1 len= 2 cap= 3
	子go程:i 2 len= 3 cap= 3
	主go程读到: 0
	主go程读到: 1
	主go程读到: 2
	主go程读到: 3
	子go程:i 3 len= 2 cap= 3
	子go程:i 4 len= 0 cap= 3
	子go程:i 5 len= 1 cap= 3
	子go程:i 6 len= 2 cap= 3
	子go程:i 7 len= 3 cap= 3
	主go程读到: 4
	主go程读到: 5
	主go程读到: 6
	主go程读到: 7
*/

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) // 写端,写完数据主动关闭channel
		//ch <- 790
	}()

	for {
		if num, ok := <-ch; ok == true {
			fmt.Println("读到数据:", num)
		} else {
			n := <-ch
			fmt.Println("关闭后:", n)
			break
		}
	}
}

  • 可以使用 range 替代 okfor num := range ch { // ch 不能替换为 <-ch
func main() {
	ch := make(chan int, 0)
	go func() {
		for i := 0; i < 5; i++ {
			ch <- i
		}
		close(ch) // 写端,写完数据主动关闭channel
		//ch <- 790
		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) // 双向channel

	var sendCh chan<- int = ch
	sendCh <- 789
	//num := <- sendCh 不能读操作

	var recvCh <-chan int = ch
	num := <-recvCh
	fmt.Println("num=", num)

	// 反向赋值
	//var ch2 chan int = recvCh
}
  • 单向channel传参传递的是引用
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) // 双向channel
	go func() {
		send(ch) // 双向channel 转为 写channel
	}()
	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) // 子go程 生产者
	consumer(ch)    // 主go程 消费
}
  • 实际开发应用
    • 在实际的开发中,生产者消费者模式应用也非常的广泛,例如:在电商网站中,订单处理,就是非常典型的生产者消费者模式
    • 当很多用户单击下订单按钮后,订单生产的数据全部放到缓冲区(队列)中,然后消费者将队列中的数据取出来发送者仓库管理等系统
    • 通过生产者消费者模式,将订单系统与仓库管理系统隔离开,且用户可以随时下单(生产数据)。如果订单系统直接调用仓库系统,那么用户单击下订单按钮后,要等到仓库系统的结果返回。这样速度会很慢
package main

import "fmt"

type OrderInfo struct { // 创建结构体类型OrderInfo,只有一个id 成员
	id int
}

func producer2(out chan<- OrderInfo) { // 生成订单——生产者
	for i := 0; i < 10; i++ { // 循环生成10份订单
		order := OrderInfo{id: i + 1}
		out <- order // 写入channel
	}
	close(out) // 写完,关闭channel
}

func consumer2(in <-chan OrderInfo) { // 处理订单——消费者
	for order := range in { // 从channel 取出订单
		fmt.Println("订单id为:", order.id) // 模拟处理订单
	}
}

func main() {
	ch := make(chan OrderInfo) // 定义一个双向 channel, 指定数据类型为OrderInfo
	go producer2(ch)           // 建新协程,传只写channel
	consumer2(ch)              // 主协程,传只读channel
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无休止符

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值