这是我参与「第三届青训营 -后端场」笔记创作活动的的第3篇笔记
😆在这里,我对今天的课所新学到的Golang并发协程同步中的WaitGroup做了一次总结
😜Golang的其他知识在哪里找呢,那你就问对了
👨💻Golang基础复习 - 掘金 (juejin.cn) 在这里我总结了一些这篇文章没有提到的一些知识
😊如果有小伙伴能想到更多知识,欢迎大家在评论区留言,那么我们就开始吧
👩💻👨💻哟西,一个棕~
😎😎😎我是小小分割线
并发编程
协程
协程不是系统级线程,很多时候协程被称为“轻量级线程”、“微线程”、“纤程(fiber)”等。
简单来说可以认为协程是线程里不同的函数,这些函数之间可以相互快速切换
Golang中创建一个协程是非常简单的,如下,在函数前加一个go关键字就能做到
go task()
通道channel
在Golang中,如果各个协程内部需要交互,传输通信,那我们该这么做到呢
Golang此时是提供了一种称为channel的机制,用于在goroutine之间共享数据
如果我们需要在goroutine之间共享数据,channel可以充当goroutine间的中间人并提供一种机制来保证同步交换
在声明通道时,我们需要给定数据类型,此时我们就可以使用这个通道了
我们goroutine的数据的交互都可以使用这个通道
在任一时间点,任一通道只能有一个数据可以访问数据或传输,因此按照设计不会发生管道内的数据竞争
根据数据交互的方式,我们可以分为:
- 有缓冲的通道
- 没有缓冲的通道
我们来看一下两者的不同
有缓冲的通道,发送方无需等待接收方把管道内的资源拿走才能放置,也即是可以做到异步通信
而无缓冲通道相反,如果接收方不接收,那么会阻塞,也即是可以做到同步通信
但是其可以保证发送和接收的瞬间执行两个goroutine之间的交互,而缓冲通道做不到这样的保证
那我们的语法是怎样的呢
我们先来看看创建channel的语法
我们需要使用到make内置函数和chan这个通道的关键字
// 都需要指定数据的类型
channel := make(chan int)
// 可以通过声明第二个参数来创造带有缓冲的通道
channelWithBuffer := make(chan int, 2)
此时我们创建了channel,那我们该怎么去使用他们呢
Golang提供了<-运算符
func main() {
// 创建了一个int类型的无缓冲的channel
channel := make(chan int)
for {
go send(channel)
receive(channel)
}
}
// 发送
func send(channel chan int) {
// 语法为:管道<-管道类型的值
channel <- 5
}
// 接收
func receive(channel chan int) {
// 关闭管道
defer close(channel)
// 语法为:<-管道
fmt.Println(<-channel)
}
/*
打印为:
5
0
panic: close of closed channel
*/
我们来了解一些Golang中channel的一些特性:
- 对于同一个channel,发送操作是互斥的,接收操作之间也是互斥的
- 发送和接收对元素的值的处理都是不可分割的
- 在完成发送前会被阻塞,接收也同样
我们来用代码看一下这些特性
func main() {
channel := make(chan int)
go send(channel)
num := <-channel
fmt.Println(num)
}
func send(channel chan int) {
// 此时等待3秒
// 接收方会被阻塞
time.Sleep(time.Second * 3)
channel <- 5
}
😎😎😎是我是我,我是小小分割线
协程同步
我们首先来看一下协程同步是什么
同步呢也就是两个协程之间会互相等待
欸?那我们刚刚不是说到不带缓冲的管道channel就是同步的吗
没错,无缓冲的channel就能做到协程同步,而且上面的代码实现也证明,在发送方完成发送前会一直阻塞
而且有缓冲的channel在缓冲区满了也可以做到协程同步
如果我们不需要channel传递数据,在其他的任务中需要协程同步,我们改怎么办呢?
我们还有什么方式能够做到协程同步呢?
sync包
在sync这个同步包中Golang官方提供了很多的同步的机制
就例如WaitGroup
WaitGroup
那么WaitGroup是什么呢?
英语翻译过来就是一个等待的一个小组
我们可以看一下他的源码的注释
// A WaitGroup waits for a collection of goroutines to finish.
// The main goroutine calls Add to set the number of
// goroutines to wait for. Then each of the goroutines
// runs and calls Done when finished. At the same time,
// Wait can be used to block until all goroutines have finished.
//
// A WaitGroup must not be copied after first use.
type WaitGroup struct {
noCopy noCopy
// 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
// 64-bit atomic operations require 64-bit alignment, but 32-bit
// compilers only guarantee that 64-bit fields are 32-bit aligned.
// For this reason on 32 bit architectures we need to check in state()
// if state1 is aligned or not, and dynamically "swap" the field order if
// needed.
state1 uint64
state2 uint32
}
我们可以得知:
-
WaitGroup等待一组协程完成。
-
主协程调用Add来设置要等待的goroutine的数量。
-
每个协程运行并在完成时调用Done。
-
可以使用Wait来阻塞,直到所有协程完成。
-
WaitGroup在第一次使用后不能被复制。
那么此时我们已经清晰的看到了WaitGroup的使用方式
有Add类的方法,还有阻塞用的Wait方法
运行结束调用Done方法,我们来看看Done是什么
// 将 WaitGroup 计数器减一
// Done decrements the WaitGroup counter by one.
func (wg *WaitGroup) Done() {
wg.Add(-1)
}
我们同步的精髓就是阻塞
我们来看一下阻塞Wait的源码
// Wait blocks until the WaitGroup counter is zero.
func (wg *WaitGroup) Wait() {
statep, semap := wg.state()
……
for {
state := atomic.LoadUint64(statep)
v := int32(state >> 32)
w := uint32(state)
if v == 0 {
// Counter is 0, no need to wait.
if race.Enabled {
race.Enable()
race.Acquire(unsafe.Pointer(wg))
}
return
}
// Increment waiters count.
……
}
我们可以看出这个方法是用来阻塞代码直到计数器为0,当计数器为0时就不再阻塞了
原来是这样,看来Group是用一个计数器来操作的吖
根据上面的说法,我们来使用一下吧
import (
"fmt"
"sync"
)
// 声明一个等待组
var wp sync.WaitGroup
func main() {
for i := 0; i < 10; i++ {
go show(i)
// 每执行一次就将计数器加一
wp.Add(1)
}
wp.Wait()
// 此时可以达到这样的效果:
// 上面的所有协程都执行完,才到这一步
fmt.Println("end...")
}
func show(i int) {
// 我们看到Done的源码其实就是 Add(-1)
// 此刻我们完成任务是就将计数器减一
defer wp.Done()
fmt.Println(i)
}
😎😎😎又是我,我是小小分割线
都用心看到这里了,那就求个赞吧😘
🥳🥳🥳如果小伙伴有其他的小知识,一定不要忘了在评论区讨论哟,多多讨论,生态才会越来越好