这是我参与「第三届青训营 -后端场」笔记创作活动的的第 2 篇笔记
1. 语言进阶
1.0 并发 vs 并行
在实际中,并行可以被理解为实现并发的一种手段。
Go可以充分发挥多核优势,高效运行。可以说Go语言就是为并发而生的。
1.1 Goroutine 协程
线程的创建、切换、停止都是很重的系统操作,是比较消耗资源的
协程可以理解为是轻量级的线程,协程的创建和调度由G语言本身实现。Go语言可以一次实现上万左右的协程。这就是为什么Go支持高并发环境。
一段使用协程的快速打印 hello goroutine 的代码
package main
import (
"fmt"
"time"
)
func hello(i int) {
println("hello goroutine: " + fmt.Sprint(i))
}
func HelloGoRoutine() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)
}
//保证子协程能够全部执行完毕
time.Sleep(time.Second)
}
func main() {
HelloGoRoutine()
}
1.2 CSP(Communicating Sequential Processes)
通过共享内存实现通信必须是通过互斥量对内存进行加锁,在这种机制下,不同的协程间会发生数据竞赛的问题,一定程度上会影响协程的性能。
1.3 Channel
无缓冲通道会使得发送信息的Goroutine和接受信息的Goroutine同步化,因此也被称为同步通道。
解决同步问题的一种方式就是使用带有缓冲区的有缓冲通道。
缓冲区满后,会发生阻塞发送,直至有消息被取出缓冲区。
示例:
当消费者的代码较为复杂,消费速度较慢时,我们就需要使用有缓冲通道实现生产者与消费者之间的通信,以防生产者的生产速度过快,影响生产者的生产效率。
也就是说有缓冲通道能够解决生产速度与消费速度不平衡带来的效率减缓问题。
package main
func CalSquare() {
src := make(chan int) //实现 AB 之间的通信
dest := make(chan int, 3) //实现 BM 之间的通信
go func() {
//A:生产数字 0~9
defer close(src) //defer 延迟执行资源关闭
for i := 0; i < 10; i++ {
src <- i
}
}()
go func() {
//B:计算输入数字的平方
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for i := range dest {
//M:复杂操作
println(i)
//顺序输出,证明并发安全
}
}
func main() {
CalSquare()
}
1.4 并发安全 Lock
前面讲了通过通信来共享内存,其实Go也保留了通过共享内存来实现通信。存在多个goroutine同时操纵一块内存区的情况,也就可能会发生数据竞赛,这时就需要互斥锁来管理内存资源。
不加锁可能会出现一个未知的结果,这就是不加锁可能会导致的并发安全问题。加锁就是并发安全的共享内存。
实际开发中的并发安全问题是有一定概率会导致错误结果出现的,比较难定位,因此我们在开发当中应该避免对共享内存做非并发安全的读写操作。
小结
-
Goroutine
- Go可以通过高效地调度模型来实现协程的高并发操作
-
Channel
- Go提倡通过通信来实现共享内存
-
Sync
- Lock
- WaitGroup
- 为了实现并发安全操作和协程间的同步
2. 实践项目
sync.Once()能够保证某个动作制备执行一次,最典型的场景就是单例对象的初始化操作。高并发情境下只执行一次的代码。
所以 Once 实现使用了一个互斥锁,互斥锁保证了只有一个 goroutine 初始化,同时采取的是双检查的机制,再次判断 Once.done 是否为 0,如果为 0,代表第一次初始化,等到初始化结束之后,再释放锁。并发情况下,其他的 goroutine 就会被阻塞在 o.m.Lock()。