这是我参与「第三届青训营 -后端场」笔记创作活动的第6篇笔记。
Go 并发编程
Go 可以充分发挥多核优势,尤其是在需要处理大量并发请求的 Web 服务器,Go 的优势体现的淋漓尽致。
并发 VS 并行
从多线程程序运行的视角来看,
并发指的是多线程程序在一个核心的 CPU 机器上运行,通过时间片的一个切换来实现同时运行的一个状态
并行是直接利用多核实现多线程的直接运行。
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 提倡通过通信共享内存而不是通过共享内存而实现通信。
通过共享内存进行通信需要互斥量对内存进行加锁,用于获取临界区的权限。这会导致数据竞态的问题,会影响程序的性能。
Channel
Channel 是一种引用类型,使用 make 创建。
- 无缓冲通道
variable := make(chan type) - 有缓冲通道
variable := make(chan type, size)
区别:
无缓冲通道也称同步通道,在通信时,发送的 Gorountine 与 接收的 Gorountine 同步。
有缓冲通道类似于生产消费模型,但缓冲区满的时候,会阻塞发送,直到缓冲区有空间。
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):计数器 + deltaDone(): 计数器减 1Wati(): 阻塞直到计数器为 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