1.并发编程
并发是多个线程在同一个CPU上运行,但是在任意时刻只有一个线程在运行。宏观上看,多个线程在同时运行,但是在微观上,每个线程都在CPU上运行,只是时间片很短,切换很快,给人的感觉就是在同时运行。并行是多个线程在多个CPU上运行,每个线程都在独立的CPU上运行,所以在任意时刻,多个线程都在同时运行。Go可以充分发挥多核CPU的优势,高效运行。
1.1 协程Goroutine
- 协程
Goroutine也叫做轻量级线程。 - 线程在创建时需要消耗一定的系统资源,线程属于内核态,线程的创建、切换、停止都属于比较重量级的系统操作,占用系统栈资源属于MB级别。
- 协程属于用户态,属于轻量级线程,协程的创建、切换、销毁都是由Go语言本身完成,占用系统栈资源属于KB级别。
- 一个线程里可以同时执行多个协程,Go可以同时创建上万级别的协程,也是Go支持高并发原因之一。
package main
import (
"fmt"
"time"
)
func hello(i int) {
println("hello world : " + fmt.Sprint(i))
}
func main() {
for i := 0; i < 5; i++ {
// 创建协程
go hello(i)
}
// 等所有协程执行结束后,主线程再结束
time.Sleep(time.Second)
}
输出结果是乱序的,说明并行输出
1.2 CSP模型
Go提倡通过通信而共享内存,这种通信模型叫做CSP模型。
共享内存实现通信:多个线程或协程可以直接访问共享的内存空间。容易引发各种并发问题,竞态条件(race condition)、死锁(deadlock)和数据竞争(data race)
1.3 通道Channel
make(chan type, capacity)创建通道- 有缓冲通道是一个生产者-消费者模型
package main
func main() {
src := make(chan int)//无缓冲通道
dest := make(chan int, 3)//有缓冲通道
// A子协程发送0 ~ 9数字到通道
go func() {
defer close(src) // 延迟关闭src通道
for i := 0; i < 10; i++ {
// 将数字发送到channel
src <- i
}
}()
// B子协程计算平方
go func() {
defer close(dest) // 延迟关闭dest通道
for i := range src {
// 将计算结果发送到channel
dest <- i * i
}
}()
// 主协程从通道接收数据
for i := range dest {
println(i)
}
}
B 子协程从 src 通道接收数字,并将计算后的结果发送到 dest 通道。
如果 dest 通道没有缓冲区,那么每次发送操作都会阻塞,直到有一个接收操作从 dest 通道中读取一个元素。
通过给 dest 通道设置缓冲区大小,可以使得 B 子协程在发送操作时不会立即阻塞,而是将元素放入缓冲区中。只有当缓冲区已满时,才会阻塞发送操作。
1.4 并发安全lock
Go加锁可以使用Mutex来实现,通过加锁可以实现多个协程在同一时间只有获取到锁的协程来运行,其他协程只能等待锁的释放,Mutex是一种互斥锁。
package main
var (
x int64
lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()// 加锁
x++
lock.Unlock()// 解锁
}
}
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x++
}
}
func main() {
// 不加锁,开启5个协程并发执行
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
println("withoutLock:", x)//8382
// 加锁,开启5个协程并发执行
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("withLock:", x)//10000
}
1.5 WaitGroup
- Go中的
WaitGroup是一个计数信号量,记录并维护运行的 goroutine。 - 如果 WaitGroup的值 > 0,Wait 方法就会阻塞,实现并发编程的同步操作。
- WaitGroup有3个方法,Add、Done、Wait。
- 开启协程:调用Add()+1
- 执行结束:调用Done()-1,Wait()会一直阻塞直到WaitGroup的值为 0。
package main
func hello(i int) {
println("hello world : " + fmt.Sprint(i))
}
func main() {
var wg sync.WaitGroup
// 开启协程+1
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
// 执行结束-1
defer wg.Done()
hello(j)
}(i)
}
// 一直阻塞,直到WaitGroup=0
wg.Wait()
}
2.依赖管理
- Go依赖管理的演进
2.1 GOPATH
环境变量GOPATH
- bin:项目编译的二进制文件 可执行程序
- pkg:项目编译的中间产物 加速编译 第三方依赖包
- src:项目源码
项目代码直接依赖src下的代码 go get 下载最新版本的包到src目录下
缺点: 没有实现package的多版本控制
2.2 Go Vendor
在项目路径下创建一个vendor目录,每个项目所需要的以来都会下载到自己的vendor目录下。 在使用包时,会先从当前项目下的vendor目录查找,然后再从GOPATH中查找, 都没有找到最后才在GO ROOT中查找
缺点: 无法控制依赖的版本,更新项目可能出现依赖冲突,导致编译出错
2.3 Go Module
- 通过go.mod文件管理依赖的版本
- 通过go get/go mod指令工具管理依赖包
2.3.1 配置文件 go.mod
package main
module example.com/foobar //依赖管理基本单元
go 1.16 //原生库
require ( //单元依赖
example.com/apple v0.1.2
example.com/banana v1.2.3
example.com/banana/v2 v2.3.4
example.com/pineapple v0.0.0-20190924185754-1b0db40df49a
)
exclude example.com/banana v1.2.4
replace example.com/apple v0.1.2 => example.com/rda v0.1.0
replace example.com/banana => example.com/hugebanana
- module:用于定义当前项目的模块路径。
- go:用于设置预期的 Go 版本。
- require:用于设置一个特定的模块版本。
- exclude:用于从使用中排除一个特定的模块版本。
- replace:用于将一个模块版本替换为另外一个模块版本。
2.3.2 依赖分发-Proxy
依赖来自于世界各地,它们使用了不同的代码托管, 这样会有一些弊端,比如:
- 无法保证构建稳定性,增加/修改/删除软件版本
- 无法保证依赖可用性,删除软件
- 增加第三方压力,平台负载问题
GoProxy,作为一个存储站点,会缓存原站中的内容,缓存中的版本也不会改变,实现了稳定可靠的依赖分发。
2.3.3 依赖分发-变量GOPROXY
可以通过设置环境变量 GOPROXY 来指定proxy服务器。
在中国大陆我们可以使用以下两个站点增加稳定性。寻址的时候会优先选择proxy1
proxy1.cn,https://proxy2.cn