[TOC]
GO 通道和 sync 包的分享
咱们一起回顾一下上次分享的内容:
- GO 协程同步若不做限度的话,会产生 数据竞态 的问题
- 咱们用锁的形式来解决如上问题,依据应用场景抉择应用互斥锁 和 读写锁
- 比应用锁更好的形式是原子操作,然而应用 go 的
sync/atomic
须要小心应用,因为波及内存
要是对 GO 的锁和原子操作还感兴趣的话,欢送查看文章 GO 的锁和原子操作分享
上次咱们分享到锁和原子操作,都能够保障共享数据的读写
可是,他们还是会影响性能,不过,Go 为开发这提供了 通道 这个神器
明天咱们来分享一下 Go 中举荐应用的其余同步办法,通道和 sync 包
通道是什么?
是一种非凡的类型,是连贯并发 goroutine
的管道
channel 通道是能够让一个 goroutine 协程发送特定值到另一个 goroutine 协程的 通信机制。
通道 像一个传送带或者队列,总是遵循 先入先出 (First In First Out)的规定,保障收发数据的程序,这一点和 管道 是一样的
一个协程从通道的一头放入数据,另一个协程从通道的另一头读出数据
每一个通道都是一个具体类型的导管,申明 channel 的时候须要为其指定元素类型。
通道能做什么?
管制协程的同步,让程序有序运行
GO 中提倡 不要通过共享内存来通信,而通过通信来共享内存
goroutine 协程 是 Go 程序并发的执行体,channel 通道就是它们之间的连贯,他们之间的桥梁,他们的交通枢纽
通道有哪几种?
大抵可分为如下三种:
- 无缓冲通道
- 有缓冲的通道
- 单向通道
无缓冲通道
无缓冲的通道又称为阻塞的通道
无缓冲通道上的发送操作会阻塞,直到另一个 goroutine 在该通道上执行接管操作,这时值能力发送胜利
两个 goroutine 协程将继续执行
咱们反过来看,如果接管操作先执行,接管方的 goroutine 将阻塞,直到另一个 goroutine 协程在该通道上发送一个数据
因而,无缓冲通道也被称为 同步通道 ,因为咱们能够应用无缓冲通道进行通信,利用发送和接管的 goroutine 协程 同步化
有缓冲的通道
还是上述提到的,有缓冲通道,就是在初始化 / 创立通道 的 make 函数 的第 2 个参数填上咱们所冀望的缓冲区大小,例如:
ch1 := make(chan int , 4)
此时,该通道的容量为 4,发送方能够始终向通道中发送数据,直到通道满,且通道数据未被读走时,发送方就会阻塞
只有通道的容量大于零,那么该通道就是有缓冲的通道
通道的容量示意通道中能寄存元素的数量
咱们能够应用内置的 len 函数 获取通道内元素的数量,应用 cap 函数 获取通道的容量
单向通道
通道默认是既能够读有能够写的,然而单向通道就是要么只能读,要么只能写
- chan <- int
是一个只能发送的通道,能够发送然而不能接管
- <- chan int
是一个只能接管的通道,能够接管然而不能发送
如何创立和申明一个通道
申明通道
在 Go 外面,channel 是一种类型,默认就是一种援用类型
简略解释一下什么是援用:
在咱们写 C ++ 的时候,用到援用会比拟多
援用,顾名思义是某一个变量或对象的别名,对援用的操作与对其所绑定的变量或对象的操作齐全等价
在 C ++ 外面是这样用的:
类型 & 援用名 = 指标变量名;
申明一个通道
var 变量名 chan 元素类型
var ch1 chan string // 申明一个传递字符串数据的通道
var ch2 chan []int // 申明一个传递 int 切片数据的通道
var ch3 chan bool // 申明一个传递布尔型数据的通道
var ch4 chan interface{} // 申明一个传递接口类型数据的通道
看,申明一个通道就是这么简略
对于通道来说,关申明了还不能应用,申明的通道默认是其对应类型的零值,例如
- int 类型 零值 就是 0
- string 类型 零值就是个 空串
- bool 类型 零值就是 false
- 切片的 零值 就是 nil
咱们还须要对通道进行初始化才能够失常应用通道哦
初始化通道
个别是应用 make 函数 初始化之后能力应用通道,也能够间接应用make 函数 创立通道
例如:
ch5 := make(chan string)
ch6 := make(chan []int)
ch7 := make(chan bool)
ch8 := make(chan interface{})
make 函数 的第二个参数是能够设置缓冲的大小的,咱们来看看源码的阐明
// The make built-in function allocates and initializes an object of type
// slice, map, or chan (only). Like new, the first argument is a type, not a
// value. Unlike new, make's return type is the same as the type of its
// argument, not a pointer to it. The specification of the result depends on
// the type:
// Slice: The size specifies the length. The capacity of the slice is
// equal to its length. A second integer argument may be provided to
// specify a different capacity; it must be no smaller than the
// length. For example, make([]int, 0, 10) allocates an underlying array
// of size 10 and returns a slice of length 0 and capacity 10 that is
// backed by this underlying array.
// Map: An empty map is allocated with enough space to hold the
// specified number of elements. The size may be omitted, in which case
// a small starting size is allocated.
// Channel: The channel's buffer is initialized with the specified
// buffer capacity. If zero, or the size is omitted, the channel is
// unbuffered.
func make(t Type, size ...IntegerType) Type
如果 make 函数 的第二个参数不填,那么就默认是无缓冲的通道
当初咱们来看看如何操作 channel 通道,都能够怎么玩
如何操作 channel
通道的操作有如下三种操作:
- 发送(send)
- 接管(receive)
- 敞开(close)
对于发送和接管通道外面的数据,写法就比拟形象,应用 <- 来指向是从通道外面读取数据,还是从通道中发送数据
向通道发送数据
// 创立一个通道
ch := make(chan int)
// 发送数据给通道
ch <- 1
咱们看到箭头的方向是,1 指向了 ch 通道,所以不难理解,这是将 1 这个数据,放入通道中
从通道中接收数据
num := <-ch
不难看出,上述代码是 ch 指向了一个须要初始化的变量,也就是说,从 ch 中读出一个数据,赋值给 num
咱们从通道中读出数据,也能够不进行赋值,间接疏忽也是能够的,如:
<-ch
敞开通道
Go 中提供了 close 函数来敞开通道
close(ch)
对于敞开通道十分须要留神,用不好间接导致程序解体
- 只有在告诉接管方 goroutine 协程所有的数据都发送结束的时候才须要敞开通道
- 通道是能够被垃圾回收机制回收的,它和敞开文件是不一样的,在完结操作之后敞开文件是必须要做的,但敞开通道不是必须的
敞开后的通道有以下 4 个特点:
- 对一个 敞开的通道 再发送值就会导致 panic
- 对一个 敞开的通道 进行接管会始终获取值直到通道为空
- 对一个 敞开的并且没有值的通道 执行接管操作会失去对应类型的 零值
- 敞开一个曾经敞开的通道会导致 panic
通道异常情况梳理
咱们来整顿一下对于通道会存在的异样:
channel 状态 | 未初始化的通道(nil) | 通道非空 | 通道是空的 | 通道满了 | 通道未满 |
---|---|---|---|---|---|
接收数据 | 阻塞 |
接收数据 | 阻塞 |
接收数据 | 接收数据 |
发送数据 | 阻塞 |
发送数据 | 发送数据 | 阻塞 |
发送数据 |
敞开 | panic | 敞开通道胜利 待数据读取结束后 返回零值 |
敞开通道胜利 间接返回零值 |
敞开通道胜利 待数据读取结束后 返回零值 |
敞开通道胜利 待数据读取结束后 返回零值 |
每一种通道的 DEMO 实战
无缓冲通道
func main() {
// 创立一个无缓冲的,数据类型 为 int 类型的通道
ch := make(chan int)
// 向通道中写入 数字 1
ch <- 1
fmt.Println("send successfully ...")
}
执行上述代码咱们能够查看到成果
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
F:/my_channel/main.go:9 +0x45
exit status 2
呈现上述报错 deadlock 谬误的起因,仔细的小伙伴应该可能晓得为什么,我上述有提到
咱们应用 ch := make(chan int)
创立的是无缓冲的通道
无缓冲的通道只有在有接管方接管值的时候能力发送数据胜利
咱们能够想一下咱们生存中的案例一样:
你在某东上买了一个略微贵重一点的物品,某东快递人员给你寄快递的时候,打电话给你,必须要送到你的手上,不然不敢签收,这个时候,你不不便,或者你不签收,那么这个快递就是算作没有寄送胜利
因而,上述问题起因是,创立了一个无缓冲通道,发送方始终在阻塞,通道中始终未有协程读取数据,导致死锁
咱们的解决办法就是创立另外一个协程,将数据从通道中读出来即可
package main
import "fmt"
func recvData(c chan int) {
ret := <-c
fmt.Println("recvData successfully ... data =", ret)
}
func main() {
// 创立一个无缓冲的,数据类型 为 int 类型的通道
ch := make(chan int)
go recvData(ch)
// 向通道中写入 数字 1
ch <- 1
fmt.Println("send successfully ...")
}
这里须要留神,如果 go recvData(ch)
放在了 ch <- 1
之后,那么后果还是一样的死锁,起因还是因为 ch <- 1
会始终阻塞,基本不会执行到 他之后的语句
实际效果
recvData successfully ... data = 1
send successfully ...
有缓冲通道
func main() {
// 创立一个无缓冲的,数据类型 为 int 类型的通道
ch := make(chan int , 1)
// 向通道中写入 数字 1
ch <- 1
fmt.Println("send successfully ...")
}
还是同样的案例,同样的代码,咱们只是把无缓冲通道,换成了有缓冲的通道,咱们依然不专门开协程读取通道的数据
实际效果,发送胜利
$$
$$
send successfully ...
因为此时通道中的缓冲是 1,第一次向通道中发送数据,不会阻塞,
可是如果,在通道中数据还未读取进来之前,又向通道中写入数据,则此处会阻塞,
若始终没有协程从通道中读取数据,则后果与上述一样,会死锁
单向通道
package main
import "fmt"
func OnlyWriteData(out chan<- int) {
// 单向 通道,只写 不能读
for i := 0; i < 10; i++ {out <- i}
close(out)
}
func CalData(out chan<- int, in <-chan int) {
// out 单向 通道,只写 不能读
// int 单向 通道,只读 不能写
// 遍历 读取 in 通道,若 in 通道 数据读取结束,则阻塞,若 in 通道敞开,则退出循环
for i := range in {out <- i + i}
close(out)
}
func myPrinter(in <-chan int) {
// 遍历 读取 in 通道,若 in 通道 数据读取结束,则阻塞,若 in 通道敞开,则退出循环
for i := range in {fmt.Println(i)
}
}
func main() {
// 创立 2 个无缓冲的通道
ch1 := make(chan int)
ch2 := make(chan int)
go OnlyWriteData(ch1)
go CalData(ch2, ch1)
myPrinter(ch2)
}
咱们模仿 2 个通道,
- 一个 只写 不能读
- 一个 只读 不能写
实际效果
0
2
4
6
8
10
12
14
16
18
敞开通道
package main
import "fmt"
func main() {c := make(chan int)
go func() {
for i := 0; i < 10; i++ {
// 循环向无缓冲的通道中写入数据,只有当上一个数据被读走之后,下一个数据能力往通道中放
c <- i
}
// 敞开通道
close(c)
}()
for {
// 读取通道中的数据,若通道中无数据,则阻塞,若读到 ok 为 false,则通道敞开,退出循环
if data, ok := <-c; ok {fmt.Println(data)
} else {break}
}
fmt.Println("channel over")
}
再次强调一下敞开通道,demo 的模仿形式与上述的案例基本一致,感兴趣的能够本人运行看看成果
看到这里,仔细的小伙伴应该能够总结出,判断通道是否敞开的 2 种 形式了吧?
- 读取通道的时候,判断 bool 类型的变量是否为 false
例如上述代码
if data, ok := <-c; ok {fmt.Println(data)
} else {break}
判断 ok 为 true,则失常读取到数据,若为 false,则通道敞开
- 通过 for range 的形式来遍历通道,若退出循环,则是因为通道敞开
sync 包
Go 的 sync 包 也是用作实现并发工作的同步
还记得吗,在分享 文章 GO 的锁和原子操作分享的时候,咱们就用到过 sync 包
用法大同音讯,这里列举一下 sync 包 波及的数据结构和办法
- sync.WaitGroup
- sync.Once
- sync.Map
sync.WaitGroup
他是一个构造体,传递的时候要传递指针,这里须要留神
他是并发平安的,外部有保护一个计数器
波及的办法:
- (wg * WaitGroup) Add(delta int)
参数中 传入的 delta,示意 sync.WaitGroup 外部的计数器 + delta
- (wg *WaitGroup) Done()
示意以后协程退出,计数器 -1
- (wg *WaitGroup) Wait()
期待并发工作执行结束,此时的计数器为变成 0
sync.Once
他是并发平安的,外部有互斥锁 和 一个布尔类型的数据
- 互斥锁 用于加锁解锁
- 布尔类型的数据 用于记录初始化是否实现
个别用于在高并发的场景下只执行一次,咱们一下子就能想到的场景会有程序启动时,加载配置文件的场景
针对相似的场景,Go 也给咱们提供了解决办法,即 sync.Once 外面的 Do 办法
- func (o *Once) Do(f func()) {}
Do 办法的参数 是一个函数,可是咱们要在该函数外面传递参数咋整?
能够应用 Go 外面的 闭包 来实现,闭包的具体实现形式,感兴趣的能够深刻理解一下
sync.Map
他是 并发平安 的,正是因为 Go 中的 map 是并发不平安的,因而有了 sync.Map
sync.Map 有如下几个显著的劣势:
- 并发平安
- sync.Map 不须要应用 make 初始化,间接应用
myMap := sync.Map{}
即可应用 sync.Map 外面的办法
sync.Map 波及的办法
见名知意
- Store
存入 key 和 value
- Load
取出 某个 key 对应的 value
- LoadOrStore
取出 并且 存入 2 个操作
- Delete
删除 key 和 对应的 value
- Range
遍历所有 key 和 对应的 value
总结
- 通道是什么,通道的品种
- 无缓冲,有缓冲,单向通道具体对应什么
- 对于通道的具体实际
- 分享了对于通道的异常情况整顿
- 简略分享了 sync 包的应用
欢送点赞,关注,珍藏
敌人们,你的反对和激励,是我保持分享,提高质量的能源
好了,本次就到这里,下一次 服务注册与发现之 ETCD
技术是凋谢的,咱们的心态,更应是凋谢的。拥抱变动,背阴而生,致力向前行。
我是 小魔童哪吒,欢送点赞关注珍藏,下次见~