Go 语言进阶——并发编程| 青训营笔记

语言: CN / TW / HK

theme: cyanosis highlight: github


这是我参与「第五届青训营」伴学笔记创作活动的第 4 天

前言

本文从并发编程的视角来了解Go高性能的本质、为何Go的运行可以如此之快,主要涉及到知识点:并发、并行、协程、CSP模型ChannelMutexWaitGroup等。

并发编程

并发VS并行

并发是指多个线程在同一个CPU上运行,主要是通过时间片的切换看起来像多个程序在同时运行,只不过CPU时间片的切换十分迅速,我们感知不到。

并行是指多个线程在多个CPU上运行,多个线程在同一时间同时运行而不是时间片的切换。

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

image.png

协程

Go实现高并发的机制里,还有一个重要的概念——协程Goroutine,协程也叫做轻量级线程

线程在创建时需要消耗一定的系统资源,线程属于内核态,线程的创建、切换、停止都属于比较重量级的系统操作,占用系统栈资源属于MB级别。

协程是属于用户态的,属于轻量级线程,协程的创建、切换、销毁都是由Go语言本身完成,比线程消耗的资源少的多,占用系统栈资源属于KB级别。

一个线程里可以同时执行多个协程,Go可以同时创建上万级别的协程,也是Go支持高并发原因之一。

image.png

代码示例

Go创建协程十分简单,只需要在调用的函数前加一个go关键字即可。

```go 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) } ```

CSP模型

CSP(Communicating Sequential Process)通信顺序进程、交谈循序程序,也被译为交换消息的循序程序,它是一种用来描述并发行系统之间进行交互的模型。

CSP最大的优点就是灵活,但也很容易出现死锁。

Go提倡通过通信共享内容而不是通过共享内存而实现通信。

image.png

通道

通过通信共享内存涉及到另一个概念——通道Channel

Go可以使用Channel控制子协程,在创建协程时需要创建同样数量的Channel

每个通道只允许交换指定类型的数据,在Go中使用chan关键字来声明一个通道,使用 close函数来关闭通道。

通过操作符<-来指定通道的方向,实现发送或接收。

通道的创建

make(chan 元素类型, [缓冲大小])

通道分类: 1. 无缓冲通道 make(chan int) 2. 有缓冲通道 make(chan int, 2)

无缓冲通道也被称为同步通道。

有缓冲通道也是一个生产-消费模型。

image.png

代码示例

下面通过一个代码示例,看一下通道的具体使用,示例通过协程和通道完成输出一个数的平方功能。

代码功能逻辑如下:

  1. A子协程发送0 ~ 9数字
  2. B子协程计算输入数字的平方
  3. 主协程输出最后的平方数

go func main() { src := make(chan int) dest := make(chan int, 3) // A子协程 go func() { defer close(src) for i := 0; i < 10; i++ { // 将数字发送到channel src <- i } }() // B子协程 go func() { defer close(dest) for i := range src { // 将计算好的数字发送到channel dest <- i * i } }() for i := range dest { println(i) } }

并发安全Lock

Go也有类似Java的锁机制,这种机制是通过共享内存来实现通讯的。

Mutex

Go加锁可以使用Mutex来实现,通过加锁可以实现多个协程在同一时间只有获取到锁的协程来运行,其他协程只能等待锁的释放,Mutex是一种互斥锁。

下面通过一个例子来看下多个协程对一个数字的相加,通过加锁与不加锁来看下两者的区别。

go 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() { 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) } 运行代码输出结果为:

withoutLock: 8113 withLock: 10000 其中withoutLock的结果每次运行结果可能不同,可以看出在不加锁的情况下,得出的结果是不满足预期结果的,也就是出现了内存不安全的资源竞争。

WaitGroup

Go中的 WaitGroup 是一个计数信号量,WaitGroupJava 中的 CyclicBarrierCountDownLatch 非常类似,可以用来记录并维护运行的 goroutine。如果 WaitGroup的值大于 0Wait 方法就会阻塞,常用来实现并发编程时的同步操作。

WaitGroup3个方法,AddDoneWait

使用方法:当开启协程时调用Add方法增加一个计数,完成时调用Done方法减去一个计数,Wait会一直阻塞直到WaitGroup的值为 0

下面使用WaitGroup来实现一个等待所有协程执行完的例子:

go func main() { var wg sync.WaitGroup // 开启5个计数 wg.Add(5) for i := 0; i < 5; i++ { go func(j int) { // 执行完时调用Donw defer wg.Done() hello(j) }(i) } // 等待所有协程执行完 wg.Wait() }

总结

本文主要涉及到知识点:并发、并行、协程、CSP模型ChannelMutexWaitGroup等。

引用

Go 语言进阶与依赖管理