Go语言并发并行与依赖管理

Go语言并发并行与依赖管理

  • 语言进阶——并发 VS 并行
    • Goroutine
    • CSP(Communicating Sequential Processes)
    • Channel
    • 并发安全LOCK
    • WaitGroup
  • 依赖管理
    • Go 依赖管理演进
      • GOPATH
      • Go Vendor
      • Go Module
    • 依赖管理三要素
      • 依赖配置
        • go.mod
        • version
        • indirect
        • incompatible
        • 依赖图
      • 依赖分发
        • 回源
        • Proxy
        • 变量GOPROXY
      • 工具

语言进阶——并发 VS 并行

并发:当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态

  • 多线程程序在一个核的cpu上运行

Go语言并发并行与依赖管理_第1张图片

并行: 当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行。

  • 多线程程序在多个核的cpu上运行
    Go语言并发并行与依赖管理_第2张图片

并行可以理解为是实现并发的一个手段,Go 语言在GOMAXPROCS数量与任务数量相等时,可以做到并行执行,但一般情况下都是并发执行。Go 可以充分发挥多核优势,高效运行。

\newline

Goroutine

  • 线程:线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
    特点:用户态轻量级线程,栈 MB 级别。
  • 协程:协程(goroutine)是一种程序组件,不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。
    特点:内核态线程跑多个协程,栈 KB 级别。
    Go语言并发并行与依赖管理_第3张图片
    一个程序可以包含多个协程,可以对比于一个进程包含多个线程。我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程,由当前协程来控制。

以下例子,快速打印hello goroutine:0~hello goroutine:4

package main

import (
	"fmt"
	"time"
)

func hello(i int) {
	println("hello goroutine : " + fmt.Sprint(i))
}

func HelloGoRoutine() {
	for i := 0; i < 5; i++ {
		go func(j int) {
			hello(j)
		}(i)
	}
	time.Sleep(time.Second)
}

func main() {
	HelloGoRoutine()
}

结果:
Go语言并发并行与依赖管理_第4张图片

CSP(Communicating Sequential Processes)

Go 实现了两种并发形式,第一种是多线程共享内存,其实就是Java或C++等语言中的多线程开发;另一种是Go语言特有的,也是Go语言推荐的CSP并发模型
Go语言并发并行与依赖管理_第5张图片
提倡通过通信共享内存而不是通过共享内存而实现通信。

\newline

Channel

如果说goroutine是Go语言程序的并发体的话,那么channels就是它们之间的通信机制。一个channels是一个通信机制,它可以让一个goroutine通过它给另一个goroutine发送值消息。
Go语言并发并行与依赖管理_第6张图片
每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。
声明通道:var 通道变量 chan 通道类型
创建通道:make(chan 数据类型, [缓冲大小])

  • 无缓冲通道,make(chan int),指在接收前没有能力保存任何值的通道,这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。
  • 有缓冲通道,make(chan int, 2),指在被接收前能存储一个或者多个值的通道,这种类型的通道并不强制要求goroutine之间必须同时完成发送和接收。

Go语言并发并行与依赖管理_第7张图片

以下实例中,A子协程发送0~9数字,B协程计算输入数字的平方,主协程输出最后的平方数。

// package concurrence
package main

func CalSquare() {
	// 创建通道
	src := make(chan int) // 创建一个整型类型的通道,无缓冲
	dest := make(chan int, 3) // 有缓冲
	// 开启一个并发匿名函数
	go func() {
		defer close(src)
		for i := 0; i < 10; i++ {
			src <- i // 通道发送数据,将值放入通道中:通道变量 <- 值
		}
	}()
	go func() {
		defer close(dest)
		for i := range src {
			dest <- i * i
		}
	}()
	// 遍历接收通道数据
	for i := range dest {
		//复杂操作
		println(i)
	}
}

func main() {
	CalSquare()
}

结果:
4.png

\newline

并发安全LOCK

并发编程的核心概念是同步通信,但是同步的方式却有多种。以下示例以互斥量 sync.Mutex 来实现同步通信,对变量执行了2000次+1操作,5个协程并发执行。

package main

import (
	"sync"
	"time"
)

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
	}
}

func Add() {
	x = 0
	for i := 0; i < 5; i++ {
		go addWithoutLock()
	}
	time.Sleep(time.Second)
	println("WithoutLock:", x)
	x = 0
	for i := 0; i < 5; i++ {
		go addWithLock()
	}
	time.Sleep(time.Second)
	println("WithLock:", x)
}

func ManyGoWait() {
	var wg sync.WaitGroup
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func(j int) {
			defer wg.Done()
		}(i)
	}
	wg.Wait()
}

func main() {
	Add()
	ManyGoWait()
}

结果:
6.png

\newline

WaitGroup

以下示例中,计数器:开启协程+1,执行结束-1,主协程阻塞直到计数器为0。
Go语言并发并行与依赖管理_第8张图片

回到前面多个协程打印hello goroutine的例子,现在用waitgroup实现协程的同步阻塞,对于这种要等待 N 个线程完成后再进行下一步的同步操作,可以使用 sync.WaitGroup 来等待一组事件。首先通过 Add() 方法,对计数器+5,用于增加等待事件的个数;然后开启协程,每个协程执行完后,调用 wg.Done() 表示完成一个事件,对计数器-1;最后 Wait() 主协程阻塞,等待全部的事件完成,计数器为0退出主协程。

func ManyGoWait() {
	var wg sync.WaitGroup
	wg.Add(5)
	for i := 0; i < 5; i++ {
		go func(j int) {
			defer wg.Done()
		}(i)
	}
	wg.Wait()
}

上述内容中,协程通过高效的调度模型实现高并发操作,通道channel通过通信实现共享内存,最后sync相关关键字实现并发安全操作和协程间的同步。

\newline

依赖管理

依赖指各种开发包,在开发项目中,需要学会站在巨人的肩膀上,利用已经封装好的、经过验证的开发组件或工具来提升自己的研发效率。
如下图,对于hello world以及类似的单体函数只需要依赖原生SDK,而实际工程会相对复杂,不可能基于标准库0~1编码搭建,而更多的关注业务逻辑的实现,而其他的涉及框架、日志、driver、以及collection等一系列依赖都会通过sdk的方式引入,这样对依赖包的管理就显得尤为重要
Go语言并发并行与依赖管理_第9张图片

\newline

Go 依赖管理演进

而Go的依赖管理主要经历了3个阶段,分别是GOPATHGo VenderGo Module,到目前被广泛应用的go module,整个演进路线主要围绕实现两个目标来迭代发展:

  • 不同环境(项目) 依赖的版本不同
  • 控制依赖库的版本

Go语言并发并行与依赖管理_第10张图片

\newline

GOPATH

GOPATH 是 Go 语言支持的一个环境变量,value 是 Go 项目的工作区,项目代码直接依赖 src 下的代码,go get 下载最新版本的包到 src 目录下。目录有以下结构:

  • src: 存放Go项目的源码。
  • pkq: 存放编译的中间产物,加快编译速度。
  • bin: 存放Go项目编译生成的二进制文件。

Go语言并发并行与依赖管理_第11张图片

以下例子,A 和 B 依赖于某一package的不同版本,就会出现问题: 无法实现package的多版本控制。
Go语言并发并行与依赖管理_第12张图片
同一个 pkg,有2个版本,A->A(),B->B(),而 src 下只能有一个版本存在,那 A,B 项目无法保证都能编译通过,也就是在 gopath 管理模式下,如果多个项目依赖同一个库,则依赖该库是同一份代码,所以不同项目不能依赖同一个库的不同版本,这很显然不能满足我们的项目依赖需求。为了解决这问题,govender 出现了。

\newline

Go Vendor

Vendor 是当前项目中的一个目录,其中存放了当前项目依赖的副本。在 Vendor 机制下,如果当前项目存在 Vendor 目录,会优先使用该目录下的依赖,如果依赖不存在,会从 GOPATH 中寻找。
以下示例中,项目目录下增加 vendor 文件,所有依赖包副本形式放在 $ProjectRoot/vendor,依赖寻址方式:vendor => GOPATH,通过每个项目引入一份依赖的副本,解决了多个项目需要同一个 package 依赖的冲突问题。

Go语言并发并行与依赖管理_第13张图片

但 vendor 无法很好解决依赖包的版本变动问题和一个项目依赖同一个包的不同版本的问题,以下示例中,项目 A 依赖 pkg B 和 C,而 B 和 C 依赖了 D 的不同版本,通过 vendo r的管理模式不能很好的控制对于 D 的依赖版本,一旦更新项目,有可能出现依赖冲突,导致编译出错。
Go语言并发并行与依赖管理_第14张图片
归根到底vendor不能很清晰地标识依赖的版本概念,于是go module应运而生。

\newline

Go Module

Go Modules 是 Go 语言官方推出的依赖管理系统,解决了之前依赖管理系统存在的诸如无法赖同一个库的多个版本等问题,go module 从 Go 1.11 开始实验性引入,Go 1.16 默认开启,一般都读为go mod。

  • 通过 go.mod 文件管理依赖包版本
  • 通过 go get/go mod 指令工具管理依赖包
  • 终极目标:定义版本规则和管理项目依赖关系

\newline

依赖管理三要素

  • 配置文件,描述依赖:go.mod
  • 中心仓库管理依赖库:Proxy
  • 本地工具:go get/mod

\newline

依赖配置

go.mod

模块路径用来标识一个模块,从模块路径可以看出从哪里找到该模块,如果是 github 前缀则表示可以从 Github 仓库找到该模块。依赖包的源代码由 github 托管,如果项目的子包想被单独引用,则需要通过单独的init go.mod 文件进行管理。以下示例中,go 1.16 为依赖的原生 sdk 版本,最下面 require 是单元依赖,每个依赖单元用模块路径+版本来唯一标示。依赖标识:[Module Path] [Version/Pseudo-version]
Go语言并发并行与依赖管理_第15张图片

\newline

version

gopath和govendor都是源码副本方式依赖,没有版本规则概念,而go mod为了方便管理则定义了版本规则,分为语义化版本和基于commit伪版本。

  • 语义化版本${MAJOR}.${MINOR}.${PATCH}。不同的 MAJOR 版本表示是不兼容的 API,所以即使是同一个库,MAJOR 版本不同也会被认为是不同的模块;MINOR 版本通常是新增函数或功能,向后兼容;而patch 版本-般是修复 bug。example:V1.3.0V2.3.0
  • 基于 commit 伪版本vx.0.0-yyyymmddhhmmss-abcdefgh1234。基础版本前缀是和语义化版本一样的:时间戳 yyyymmddhhmmss,也就是提交 Commit 的时间,最后是校验码 abcdefabcdef,包含 12 位的哈希前缀,每次提交 commit 后 Go 都会默认生成一个伪版本号。example:v0.0.0-20220401081311-c38fb59326b7V1.0.0-20201130134442-10cb98267c6c

\newline

indirect

以下示例中,依赖单元中的特殊标识符——indirect后缀,表示go.mod对应的当前模块,没有直接导入该依赖模块的包,也就是非直接依赖,标示间接依赖
Go语言并发并行与依赖管理_第16张图片
Go语言并发并行与依赖管理_第17张图片

\newline

incompatible

主版本2+模块会在模块路径增加/VN后缀,这能让 go module 按照不同的模块来处理同一个项目不同主版本的依赖。由于 go module 是 1.11 实验性引入,所以这项规则提出之前已经有一些仓库打上了2或者更高版本的tag了,为了兼容这部分仓库,对于没有go.mod文件并且主版本在2或者以上的依赖会在版本号后加上+incompatible 后缀
Go语言并发并行与依赖管理_第18张图片

\newline

依赖图

如果 X 项目依赖了 A、B 两个项目,且 A、B 分别依赖了 C 项目的 v1.3、V1.4 两个版本,最终编译时所使用的 C 项目的版本为如下哪个选项? (B)
A. v1.3
B. V1.4
C.A用到C时用v1.3编译,B用到C时用v1.4编译
Go语言并发并行与依赖管理_第19张图片
选择最低兼容版本。

\newline

依赖分发

回源

gomodule的依赖分发,也就是从哪里下载,如何下载的问题。
github是比较常见给的代码托管系统平台,而 Go Modules 系统中定义的依赖,最终可以对应到多版本代码管理系统中某一项目的特定提交或版本,这样的话,对于go.mod中定义的依赖,则直接可以从对应仓库中下载指定软件依赖,从而完成依赖分发。
Go语言并发并行与依赖管理_第20张图片
但直接使用版本管理仓库下载依赖,存在多个问题:

  • 无法保证构建确定性:软件作者可以直接代码平台增加修改/删除 软件版本,导致下次构建使用另外版本的依赖,或者找不到依赖版本。
  • 无法保证依赖可用性:依赖软件作者可以直接代码平台删除软件,导致依赖不可用。
  • 大幅增加第三方代码托管平台压力

\newline

Proxy

go proxy 可以解决上述的一些问题,Go Proxy 是一个服务站点,它会缓存源站中的软件内容,缓存的软件版本不会改变,并且在源站软件删除之后依然可用,从而实现了供 "immutability““available” 的依赖分发;使用 Go Proxy 之后,构建时会直接从 Go Proxy 站点拉取依赖。类比项目中,下游无法满足上游的需求,可以使用 Proxy 来解决。
Go语言并发并行与依赖管理_第21张图片

\newline

变量GOPROXY

Go Modules 通过 GOPROXY 环境变量控制如何使用 Go Proxy; GOPROXY="https://proxy1.cn, https://proxy2.cn, direct" GOPROXY 是一个 GoProxy 站点 URL 列表,可以使用 “direct” 表示源站。对于示例配置,整体的依赖寻址路径,会优先从 proxy1 下载依赖,如果 proxy1 不存在,会到 proxy2 寻找,如果 proxy2 中不存在,则会回源到源站直接下载依赖,缓存到 prox 站点中

21.png

\newline

工具

以下介绍go module的管理工具:

  • go get

Go语言并发并行与依赖管理_第22张图片

  • go mod
    尽量提交之前执行下go tidy,减少构建时无效依赖包的拉取。

Go语言并发并行与依赖管理_第23张图片

你可能感兴趣的:(go,后端,golang,后端,开发语言)