这是我参与「 第五届青训营 」伴学笔记创作活动的第 2 天
Go语言并发
并发与并行
- 并发:多个线程通过切换时间片的方式在一个
cpu上进行调度运行 - 并行:多个线程在
cpu的多个核上运行,真正的同时运行
线程与协程
- 线程:内核态,操作系统内核进行调度的基本单位,在一个线程上可以跑多个携程,栈大小在MB级别
- 协程:用户态,由
Go管理,轻量级线程,栈大小在KB级别
Go中使用go语句创建协程
package concurrence
import (
"fmt"
"sync"
"time"
)
func hello(i int) {
println("hello world : " + fmt.Sprint(i))
}
func ManyGo() {
for i := 0; i < 5; i++ {
go func(j int) {
hello(j)
}(i)
}
time.Sleep(time.Second)
}
输入如下
可以看到输出并不是顺序的,说明确实协程是并发执行的
CSP
- 通过通信共享内存:通过通道的方式实现进程之间信息的交换(
Go语言提倡) - 通过共享内存实现通信:操作系统中进程通信的经典方式,通过读写信号量实现对临界区内存的正确访问
Channel
make(chan <eleType>, [size])
- 无缓冲通道:
make(chan int) - 有缓冲通道:
make(chan int, 2)
package concurrence
func CalSquare() {
src := make(chan int)
dest := make(chan int, 3)
// 协程A: 将数字放进无缓冲通道src中
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
// 协程B: 将数字从src中取出,平方后放入有缓冲通道dest
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
// 主线程: 将数字从dest中取出并打印
for i := range dest {
println(i)
}
}
Lock
使用信号量的方式对临界区的访问进行控制,使得并发的协程能够正确访问临界区
package concurrence
import "sync"
var (
x int64
lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}
测试是各创建5个协程调用上述两个方法,结果如下
可以看到加锁方法可以保证每次都正确得到结果;但是不加锁的方法每次得到的结果是不确定的
WaitGroup
前面在主线程创建了协程之后,主线程是使用time.Sleep()方法来阻塞自己的,但是这并不是一个好的方法,因为我们并不知道协程到底什么时候执行结束,我们只能传入一个大概的比较大的值进去。
sync包下有一个结构体:WaitGroup,可以通过该方法优雅地实现主进程的阻塞
该结构体内部维护了一个计数器,并且暴露了三个方法出来
Add:创建了多少个协程,就传入相应的deltaDone:当协程运行结束时,调用Done()Wait:该方法用来阻塞直到所有的协程执行结束
然后就可以使用这三个方法来实现主进程的阻塞(以第一个例子为例)
func ManyGo() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(j int) {
defer wg.Done()
hello(j)
}(i)
}
wg.Wait()
}
Go依赖管理
依赖管理演进
GOPATH
在GOPTAH指定工作空间的模式下,所有的项目代码都要存放在$GOPATH/src目录下,包括项目代码依赖的包也是存放在该目录下,就会导致下面的问题:两个项目依赖于两个不同版本的包,但是由于两个版本的包不兼容就导致两个项目会有一个有问题,即 无法实现package的多版本控制
Go Vender
项目目录下增加vendor文件,在每个项目内配置该项目所依赖的包
依赖寻址方式:先vender 后 GOPATH
这个方式也会存在一个问题:一个项目依赖于两个package,这两个package又依赖于不同版本的package,这样还是会导致冲突。
Go Module
实现了终极目标:定义版本规则和管理项目依赖关系
- 通过
go.mod文件管理依赖包版本 - 通过
go get/go mod指令工具管理依赖包
配置文件——go.mod
可以看到依赖由几部分组成:包路径、版本号、以及一些特殊标志
版本号
- 语义化版本:
${MAJOR}.${MINOR}.${PATCH} - 基于
commit的伪版本:vx.0.0-yyyymmddhhmmss-<12位hash码>
特殊标志
indirect:表示的是间接依赖。A->B->C,A对C就是间接依赖- 主版本
2+模块会在模块路径后增加/vN后缀 +incompatible:对于没有go.mod文件且主版本2+的依赖,表示可能出现不兼容的代码逻辑
依赖图
最终编译的时候会使用C 1.4:因为会选择最低兼容版本
Proxy
对于go.mod所用到的依赖应该去哪里下载以及如何下载,常见的有Github等第三方代码托管平台,像之前在配置Go语言开发环境时就需要到Github上下载对应的依赖工具。但是依赖第三方代码托管平台上下载依赖会存在一些问题:
- 无法保证构建稳定性以及依赖可用性:因为作者随时可以修改删除软件版本甚至是整个软件
- 增加第三方平台压力
一个解决方案就是引入Proxy:它其实是一个服务站点,用来缓存源站中的软件以及对应的软件版本,不会改变,实现依赖的稳定性和可靠性。
使用GOPROXY配置,指定一个服务站点url列表。例如:GOPROXY="https://proxy1.cn, https://proxy2.cn, direct(源站点)",那么就会依次到指定几个网站中下载依赖。
go get/go mod
使用go get/go mod来管理和安装项目的依赖