Go语言并发与依赖管理 | 青训营笔记

78 阅读2分钟

这是我参与「 第五届青训营 」伴学笔记创作活动的第 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:创建了多少个协程,就传入相应的delta
  • Done:当协程运行结束时,调用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文件,在每个项目内配置该项目所依赖的包

依赖寻址方式:先venderGOPATH

这个方式也会存在一个问题:一个项目依赖于两个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->CAC就是间接依赖
  • 主版本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来管理和安装项目的依赖