这是我参与「第五届青训营 」伴学笔记创作活动的第 2 天
前言
这是跟随青训营学习的第二天,写文章的目的就是将今天学到的知识融会贯通,复习提升。也希望通过这样的方式与青训营的诸位一起进步!
并发编程
为什么go语言的速度为什么这么快?
协程goroutine
线程和协程的通俗说明:
通俗易懂的讲,线程是操作系统的资源,以Java为例,当java程序创建一个线程,虚拟机会向操作系统请求创建一个线程,虚拟机本身没有能力创建线程。而线程又是昂贵的系统资源,创建、切换、停止等线程属性都是重量级的系统操作,非常消耗资源,所以在java程序中每创建一个线程都需要经过深思熟虑的思考,否则很容易把系统资源消耗殆尽。
而协程,看起来和线程差不多,但创建一个协程却不用调用操作系统的功能,编程语言自身就能完成这项操作,所以协程也被称作用户态线程。我们知道无论是java还是go程序,都拥有一个主线程,这个线程不用显示编码创建,程序启动时默认就会创建。协程是可以跑在这种线程上的,你可以创建多个协程,这些协程跑在主线程上,它们和线程的关系是一对多。如果你要创建一个线程,那么你必须进行操作系统调用,创建的线程和主线程是同一种东西。显然,协程比线程要轻量的多。
与其他大部分语言提供的协程支持相同,Go 的 Goroutine 是用户态的,其协程栈占用仅有 KB 级别,十分节约系统资源;但不同的是,Goroutine 将协程和并发简化到了仅需一个 go 关键字即可完成,而不像其他语言的协程一样及其繁琐复杂。示例如下:
import (
"fmt"
"time"
)
func HelloPrint(i int) {
println("Hello goroutine : " + fmt.Sprint(i))
}
// 效果就是快速且无序打印
func HelloGoroutine() {
for i := 0; i < 5; i++ {
go func(j int) {
HelloPrint(j)
}(i)
}
// time.Sleep()的作用是:保证了子协程在执行完之前,主协程不退出。
time.Sleep(time.Second)
}
func main() {
HelloGoroutine()
}
通道Channel
go语言提倡通过通信共享内存而不是通过内存共享通信!
通道是用来传递数据的一个数据结构,通过传递指定类型的值来同步通讯
操作符<-用于指定通道的方向,实现发送or接收
通道定义:
make(chan 元素类型,[缓冲大小])
示例如下:
import (
"fmt"
)
func CalcPow() {
src := make(chan int)
dest := make(chan int, 3)
// 子协程src发送0~9数字
go func() {
defer close(src) // 当子协程src结束的时候再关闭,减少资源浪费
for i := 0; i < 10; i++ {
src <- i
}
}()
// 子协程dest计算输入数字的平方
go func() {
defer close(dest)
// 通过 range 关键字来实现遍历读取到的数据
for i := range src {
dest <- (i * i)
}
}()
// 主协程输出最后的答案
// 这里可以暂时认为子协程需要使用匿名函数
for i := range dest {
// 因为主协程可能会有更多的复杂操作,比较耗时,所以用带缓冲的通道可以避免问题
fmt.Println(i)
}
}
func main() {
CalcPow()
}
并发安全问题与LOCK
我们先看一个简单的程序:
func addWithoutLock() {
for i := 0; i < 2000 ; i++ {
x += 1
}
}
func main(){
x = 0
for i := 0; i < 5; i++ {
go addWithoutLock()
}
time.Sleep(time.Second)
println("WithoutLock:",x)
}
程序很简单,就是5个协程并发执行,每个协程计数到2000,理论上结果应该是10000,但最终结果会比10000小,为什么?
当多个线程同时执行,多个线程之间是相互抢占资源执行,并且抢占是发生在线程的执行的每一步过程中,导致出现非法数据。这种现象就称之为多线程的并发安全问题
如何解决呢?我们需要一把“锁”,引入并发锁 sync.Mutex。当有协程操作资源时,将该资源锁住,不让其他协程使用。例如:
x int64
lock sync.Mutex
)
func addWithLock() {
for i := 0; i < 2000; i++ {
lock.Lock()
x += 1
lock.Unlock()
}
}
func main() {
x = 0
for i := 0; i < 5; i++ {
go addWithLock()
}
time.Sleep(time.Second)
println("WithLock:", x)
}
并发锁的使用要十分谨慎,请尽在有并发安全的代码中使用并发锁,因为并发锁的使用实际上将并行的程序串行化,会导致显著降低性能;同时,不当的锁使用也可能导致死锁(DeadLock)等问题发生,即两个协程互相锁住对方需要之后操作的资源,导致代码卡死。
WaitGroup实现同步
import (
"fmt"
"sync"
)
func HelloPrint(i int) {
fmt.Println("Hello WaitGroup :", i)
}
func ManyGoWait() {
var wg sync.WaitGroup
wg.Add(5) //Add方法:计数器加5
for i := 0; i < 5; i++ {
go func(j int) {
defer wg.Done() //计数器—1
HelloPrint(j)
}(i)
}
wg.Wait()//阻塞直到计数器为0
}
func main() {
ManyGoWait()
}
依赖管理
最早的时候,Go所依赖的所有的第三方库都放在GOPATH这个目录下面。这就导致了同一个库只能保存一个版本的代码。如果不同的项目依赖同一个第三方的库的不同版本,应该怎么解决?
go依赖管理的演进
GOPATH
是go语言支持的环境变量,有如下三个部分:
- bin:项目编译的二进制文件
- pkg:项目编译的中间产物,加速编译
- src:项目源码,项目代码直接依赖src下的代码
go get下载最新版本的包到src目录下
缺点:无法实现package的多版本控制。
Go Vendor
通过在项目目录下新建 vendor 文件夹,并存放依赖库文件副本的方式,使得不同项目可以依赖不同的依赖库版本,解决了版本冲突的问题。
但是,他更新项目的时候可能导致编译错误的冲突且无法控制依赖的版本。
Go Module
- 通过
go.mod文件管理依赖包版本 - 通过
go get/go mod指令工具管理依赖包
依赖管理的三要素
- 配置文件,描述依赖——
go.mod - 中心仓库管理依赖库——
Proxy - 本地工具——
go get/mod
go mod
version
version的两种类型:
- 语义化版本
- 基于commit版本
indirect
表示该模块没有直接依赖
incompatible
表示要按照不同的模块来处理相同项目不同主版本的依赖
依赖分发,回源
即依赖从哪里下载,如何下载的问题
变量 GOPROXY
整体的依赖寻址路径,会先从proxy1开始,若没有,去proxy2,若无,去原站