并发编程与依赖管理|青训营

71 阅读2分钟

1.并发编程

  • 并发是多个线程在同一个CPU上运行,但是在任意时刻只有一个线程在运行。宏观上看,多个线程在同时运行,但是在微观上,每个线程都在CPU上运行,只是时间片很短,切换很快,给人的感觉就是在同时运行。
  • 并行是多个线程在多个CPU上运行,每个线程都在独立的CPU上运行,所以在任意时刻,多个线程都在同时运行。
  • Go可以充分发挥多核CPU的优势,高效运行。

img.png

1.1 协程Goroutine

img_1.png

  • 协程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模型。

img_2.png 共享内存实现通信:多个线程或协程可以直接访问共享的内存空间。容易引发各种并发问题,竞态条件(race condition)、死锁(deadlock)和数据竞争(data race)

1.3 通道Channel

  • make(chan type, capacity)创建通道
  • 有缓冲通道是一个生产者-消费者模型

img_4.png

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依赖管理的演进 img_5.png

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

img_6.png 可以通过设置环境变量 GOPROXY 来指定proxy服务器。 在中国大陆我们可以使用以下两个站点增加稳定性。寻址的时候会优先选择proxy1 proxy1.cn,https://proxy2.cn

2.3.4 工具-go get

img_7.png

2.3.5 工具-go mod

img_8.png