Go 并发编程 | 青训营笔记

79 阅读3分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第6篇笔记。

Go 并发编程

Go 可以充分发挥多核优势,尤其是在需要处理大量并发请求的 Web 服务器,Go 的优势体现的淋漓尽致。

并发 VS 并行

从多线程程序运行的视角来看,

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

image-20220510000637421

并行是直接利用多核实现多线程的直接运行。

image-20220510000708102

Goroutine

Go 通过协程实现高并发。

  • 协程:用户态,轻量级线程,栈 KB 级别

  • 线程:内核态,线程跑多个协程,栈 MB 级别

不同于线程由系统管理,协程的创建和调度由 Go 本身去执行。

在 Go 中,使用 go 关键字创建一个协程。

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()
}
hello goroutine: 4
hello goroutine: 2
hello goroutine: 3
hello goroutine: 0
hello goroutine: 1

CSP (Communicating Sequential Processes)

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

image-20220510004216079

通过共享内存进行通信需要互斥量对内存进行加锁,用于获取临界区的权限。这会导致数据竞态的问题,会影响程序的性能。

Channel

Channel 是一种引用类型,使用 make 创建。

  • 无缓冲通道 variable := make(chan type)
  • 有缓冲通道 variable := make(chan type, size)

区别:

无缓冲通道也称同步通道,在通信时,发送的 Gorountine 与 接收的 Gorountine 同步。

有缓冲通道类似于生产消费模型,但缓冲区满的时候,会阻塞发送,直到缓冲区有空间。

image-20220510005456902

package main

/*
 A 子协程发送 0 ~ 9
 B 子协程计算输入数字的平方
 M 主协程输出最后的平方
*/

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

// 0 1 4 9 16 25 36 49 64 81

并发安全 Lock

Go 也要保留了通过共享内存通信的机制。

使用 sync.Mutex 获取一个锁,使用 LockObject.Lock() 上锁,使用 LockObject.Unlock() 解锁。

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 main() {
	Add()
}

//WithoutLock: 8102
//WithLock: 10000

WaitGroup

使用 WaitGroup 实现线程的同步,在 sync 包下。

WaitGroup 对外暴露了三个方法:

  • Add(delta int) :计数器 + delta
  • Done() : 计数器减 1
  • Wati() : 阻塞直到计数器为 0
package main

import (
	"fmt"
	"sync"
)

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

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

func main() {
	ManyGoWait()
}

//hello goroutine: 4
//hello goroutine: 3
//hello goroutine: 0
//hello goroutine: 2
//hello goroutine: 1