这是我参与「第五届青训营 」笔记创作活动的第2天
本堂课重点内容
- Go语言同步机制
- Go语言依赖管理
详细知识点介绍
Go并发
协程
Go语言支持轻量级、用户态的协程,可以一次创建很多个。要在Go语言中创建一个协程,只需在要执行的函数前加上go即可。
同步
channel
可以使用channel实现线程间的同步。在我看来channel类似于一个生产者-消费者模型,生产者往管道写入,消费者从管道取出。要是管道已满,生产者就阻塞;要是管道为空,消费者就阻塞。
如果channel的缓冲很大的话,就可以看成是一个信号量。写入一个元素就相当于signal,读取一个元素相当于是wait。这样一来就能用channel解决各种同步互斥问题了。(刚刚考完操作系统,强行生搬硬套一下hh)
当然channel在此之外也能够起到进程间通信的效果。
lock
最基本的锁,没啥好说的。Go还支持读写锁。
WaitGroup
可以用于主线程等它创建的多个协程。主线程创建多少个就Add多少,然后用wait阻塞。每个协程执行完后Done一下。所有协程执行完后主线程才继续执行。
Go依赖管理
GoPath
最初的管理方式,所有依赖代码都放在这里的文件夹下面。无法实现多package的版本控制。
Go Vender
依赖放在项目文件夹的vender目录下。对于复杂的依赖关系仍然无法处理。
Go Mod
用项目目录下的go.mod文件维护依赖信息。 常用命令:
go mod init:初始化go mod文件go mod tidy:更新go mod文件go mod download:下载缺少的依赖
实践练习例子
为啥需要同步?
func main() {
a := 0
var w sync.WaitGroup
w.Add(1000)
for i := 0; i < 1000; i++ {
go func() {
a++
w.Done()
}()
}
w.Wait()
fmt.Println(a)
}
以上这段代码,某次运行结果是987,第二次又变成990了,而且都不等于创建协程的个数1000。这是因为a++其实并不是原子操作,例如它可以被分解为三条指令:
- reg <- a
- reg <- reg + 1
- a <- reg
其中reg是寄存器。倘若a、b两个协程按照a.1 -> b.1 -> a.2 -> b.2 -> a.3 ->b.3的顺序执行,那么在两个协程运行结束后,a的值是a+1而不是a+2。这就是因为缺少同步机制,不同线程的指令交织在一起运行。
解决方法就是引入同步机制,让一个协程完整运行完a++后,再开始下一个。
var l sync.Mutex
for i := 0; i < 1000; i++ {
go func() {
l.Lock()
a++
l.Unlock()
w.Done()
}()
}
但很显然,这会降低程序的并发性,因此需要权衡二者。