Go语言快速上手-语言进阶

并发编程

并发

并发:指多线程程序在一个核的CPU上运行,通过时间片的切换,实现同时运行的状态

广义的并发,是指系统的一个特征

并行

并行:利用多核、实现多线程程序的运行

并行可以看作是实现并发的一个手段

Go可以充分发挥多核优势,高效运行

Goroutine

协程:用户态,轻量级线程,创建、调度由Go语言本身完成,栈MB级别

线程:内核态,线程并发跑多个协程,栈KB级别

下面是一个快速打印hello goroutine : 0 ~ hello goutine : 4的例子

如果不考虑快速二字,直接通过for循环的串行打印即可完成

开启协程只需在调用函数的时候,函数前面加一个go关键字,这就可以为函数船舰协程去运行

下面是一个Goroutine快速打印的例子package main

import (
“fmt”
“time”
)

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

func HelloGoRoutine() {
for i := 0; i < 5; i++ {
//函数前加go关键字,调用协程
go func(j int) {
hello(j)
}(i)
}
//保证子协程执行完之前主协程不退出
time.Sleep(time.Second)
}

// 快速打印hello goroutine : 0 ~ hello goutine : 4
func main() {
HelloGoRoutine()
}

运行结果可以看出,输出结果是乱序的,因为是通过并行输出

CSP

CSP(Communicating Sequential Processing)

协程间的通信,Go提倡通过通信来共享内存,而不是通过共享内存来通信

通过通信共享内存,就要提到Chanel(通道)的概念。

左图是通道共享内存的示意图,Goroutine是程序并发的一个执行体,Chanel相当于把goroutine连接起来,相当于一个传输队列,遵循FIFO(先入先出),保证收发数据的顺序。

Chanel是一个Goroutine发送一个特定的值到另一个Goroutine的通讯机制。

Go也保留着共享内存来实现通讯的机制(如右图),我们会通过共享内存来进行数据交换。 这种情况需要通过互斥链对内存进行加锁,获取临界区的权限,一定程度上这种方法会影响程序的性能。

Channel

Channel是一种引用类型, 通过make关键字进行创建make(chan 元素类型,[缓冲大小])

下面是int类型的有缓冲和无缓冲通道:

  • 无缓冲通道:make(chan int)
  • 有缓冲通道:make(chan int,2)//容量为2的有缓冲通道

使用无缓冲通道通讯时,会导致发的goroutine和接受的goroutine同步化,因此也被称为同步通道

解决同步问题就要使用有缓冲区的有缓冲通道,容量为2,即说明缓冲区可以存放2个元素,如果存不下了,就会阻塞发送的goroutine,是一个典型的生产者消费者问题package main

// A 子协程发送0-9数字
// B 子协程计算A协程传来数字的平方
// 主协程接受B子协程计算结果,输出最后的平方数
func main() {
CalSquare()
}

func CalSquare() {
//无缓冲队列
src := make(chan int)
dest := make(chan int, 3)
//第一个协程生产数字
go func() {
defer close(src)
for i := 0; i < 10; i++ {
//将生产的数字发送到Chanel
src <- i
}
}()

//第二个携程计算平方数
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()

//主协程输出结果
for i := range dest {
//可能出现的复杂操作
println(i)
}
}

  • 通过src、dest、chanel可以保证顺序性,也就是并发安全的
  • 通过defer实现延迟的资源关闭
  • B协程使用带缓冲的通道,是因为B协程的处理复杂,会导致消费速度慢,解决这个问题就要使用带缓冲的通道去解决生产和消费速度不均衡带来的执行效率问题

并发安全Sync.Lock

Go也保留了共享内存实现通讯的方式,这样就会导致,很多goroutine公用一块内存区域的情况,也就是数据竞态问题package main

import (
“sync”
“time”
)

var (
x   int64
lock sync.Mutex //互斥链关键字
)

// 对变量执行2000次的+1操作
func main() {
Add()
}

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 addWithLock() {
for i := 0; i < 2000; i++ {
//先获取临界区的访问权限(获取资源)
lock.Lock()
x += 1
//计算完将临界区释放
lock.Unlock()
}
}

// 没有临界区保护
func addWithoutLock() {
for i := 0; i < 2000; i++ {
x += 1
}
}

执行结果说明,加锁的协程得到了预期值,而未加锁的协程会计算出一个随机数

这就是并发安全问题,实际开发中,有一定概率会触发错误结果的出现,并且很难排查

因此在开发中应避免非并发安全的临界区读写操作

Sync.WaitGroup

在上面两个例子中,都用了Sleep来进行协程的阻塞,这样并不是优雅的,因为我们无法预知协程的执行时间

Go语言中可以使用WaitGroup来实现并发任务的同步,也是在Sync包下,主要有三个方法:

  • Add(delta int):计数器+delta
  • Done():计数器-1
  • Wait():阻塞直到计数器为0(说明所有的子协程都执行结束了)

计数器:开始协程+1,执行结束-1,主协程阻塞直到计数器为0

在前面goroutine的案例中,乱序输出了0-4

下面是使用WaitGroup优化的代码package main

import (
“fmt”
“sync”
)

func main() {
ManyGoWait()
}

func ManyGoWait() {
var wg sync.WaitGroup
//创建五个协程
wg.Add(5)
for i := 0; i < 5; i++ {
go func(j int) {
//子协程执行结束,计数器-1
defer wg.Done()
hello2(j)
}(i)
}
//主协程阻塞
wg.Wait()
}
func hello2(i int) {
println(“hello goroutine : ” + fmt.Sprint(i))
}

依赖管理

依赖管理演进

依赖指的是各种开发包,开发项目中需要学会利用已经封装好的开发组件、工具来提升开发效率

Go依赖管理演进:GOPATH->Go Vendor->Go Module

现在广泛应用的,是Go Module

GOPATH

$GOPATH:是Go项目的一个工作区Workspace

  • bin:项目编译的二进制文件
  • pkg:项目编译的中间产物,加速编译
  • src:项目源码

项目代码直接依赖src下的代码

go get下载的最新版本的包到src目录下

弊端

假设有两个项目A和,都依赖同一个pkg,但是同一个pkg有两个版本

一个是v1一个是v2

v1实现了A()方法,v2实现了B()方法

v2高版本把A()删掉了,没有做向下兼容

因为都依赖同一个源码,所以不能保证AB项目同时编译成功

也就是在gopath管理模式下,如果多个项目依赖同一个库,则依赖该库是同一份代码,所以不同项目不能依赖同一个库的不同版本,这很显然不能满足我们的项目依赖需求。为了解决这问题,govender出现了,

Go Vendor

该项目目录下增加Vendor文件,所有依赖包副本形式放在ProjectRoot/vendor

依赖寻址方式:vendor=>GOPATH(Vendor下没有就去GOPATH下找)

通过每个项目引入一份依赖的副本,解决了多个项目需要同一个package依赖的冲突问题

弊端

ProjectA依赖了B和C两个包,而B和C都引入了D依赖

D依赖具有V1和V2两个版本

这时Vendor就不能很好的控制v1和v2的版本选择问题,可能会出现依赖冲突,导致无法编译

  • 无法控制依赖版本
  • 更新项目有可能出现依赖冲突,导致编译出错

Go Module

Go Module的依赖管理系统解决了依赖版本管理的问题

  • 通过go.mod文件管理依赖包版本
  • 通过go get/go mod指令工具管理依赖包

终极目标:定义版本规则和管理项目的依赖关系

依赖管理三要素

  1. 配置文件,描述依赖:go.mod
  2. 中心仓库管理依赖库:Proxy
  3. 本地工具:go get/mod(相当于Java中的Maven)

依赖配置

go.mod

  • 第一行标识了模块路径
    • 如果一个项目很复杂,涉及到了很多包,每个包单独被引用,则需要每个包里都需要建立一个go.mod文件
  • 第三行是原生库的版本
  • require后面的内容都是单元依赖

version

gopath和govendor都是源码副本方式依赖,没有版本规则概念,而gomod为了放方便管理则定义了版本规则,并分为了两种类型

  • 语义化版本
  • 基于commit的伪版本

语义化版本:**${MAJOR}.${MINOR}.${PATCH}**

  • MAJOR:大版本,不同的大版本可互相不兼容(代码隔离的)
  • MINOR:新增函数或者功能,基于MAJOR保证兼容
  • PATCH:bug修复

语义化版本来源于git的tag概念

基于commit的伪版本:版本前缀+commit的时间戳+12位哈希码前缀

在github中每次提交都会自动生成一个伪版本号

indirect

假设一个依赖关系:A->B->C

则:A->B直接依赖,A->C间接依赖

非直接依赖会被标记为indirect

incompatible

主版本2+模块会在模块路径增加/vN后缀,这能让go module按照不同的模块来处理同一个项目不同主版本的依赖。

由于gomodule是1.11实验性引入所以这项规则提出之前已经有一些仓库打上了2或者更高版本的tag了,为了兼容这部分仓库,对于没有go.mod文件并且主版本在2或者以上的依赖,会在版本号后加上+incompatible 后缀

也就是说,打上此标签的依赖,可能会出现一些问题

依赖图

在Go依赖管理中会通过选择最低的兼容版本

因为C的MAJRO都是1,版本是兼容的,则选择最低的兼容版本1.3

依赖分发

依赖分发表示:依赖去哪里下载,如何去下载的问题

回源

Github:代码托管系统平台,Go Module中定义的依赖都可以对应到代码仓库管理系统中某个项目的特定提交,go.mod中定义的依赖直接可以从对应仓库中下载

直接使用版本管理仓库下载依赖可能会存在一些问题:

  • 无法保证构建稳定性(增加、修改、删除软件版本)
  • 无法保证依赖稳定性(如删除软件)
  • 增加第三方压力(代码托管平台负载问题)

Proxy

Go Proxy是一个服务站点,会缓存原网站中的软件内容,如果原网站删除了某个版本,或者仓库,缓存的软件不会发生改变,比较稳定可靠。

这套方案可以类比到实际项目中,如适配器等……

变量GOPROXY

Go mod通过GOPROXY变量来控制proxy的配置,它是一个URL列表,逗号分隔GOPROXY=”https://proxy1.cn,https://proxy2.cn,direct”

direct表示源站

当我们试图引入一个依赖的时候,会按照:proxy1,proxy2…的顺序查找依赖,都没找到的时候会回源到第三方原站上去。

这个模式和系统设计中,设计缓存的场景中是一致的

可以加入一些本地缓存、分布式缓存,最终依赖DB这种模式。

工具

go get

go get example.org/pkg@

  • @update:默认
  • @none:删除依赖
  • @v1.1.2:tag版本,语义版本
  • @23dfdd5:特定的commit
  • @master:分支的最新commit

测试

项目实战

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇