[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
关闭通道
close(ch)
对于关闭通道非常需要注意,用不好直接导致程序崩溃
关闭后的通道有以下 4 个特点:
通道异常情况梳理
我们来整理一下对于通道会存在的异常:
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 ,则通道关闭
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
总结
欢迎点赞,关注,收藏
好了,本次就到这里,下一次 服务注册与发现之 ETCD
技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。
我是小魔童哪吒,欢迎点赞关注收藏,下次见~