不会飞的章鱼

熟能生巧,勤能补拙;念念不忘,必有回响。

Go高级工程师_第6课_Go并发编程最佳实践

并发内置数据结构

sync.Once

sync.Once 只有⼀个⽅法,Do()

但 o.Do 需要保证:

  • 初始化⽅法必须且只能被调⽤⼀次
  • Do 返回后,初始化⼀定已经执⾏完成

sync.Pool

主要在两种场景使⽤:

  • 进程中的 inuse_objects 数过多,gc mark 消耗⼤量 CPU
  • 进程中的 inuse_objects 数过多,进程 RSS 占⽤过⾼

请求⽣命周期开始时,pool.Get,请求结束时,pool.Put。在 fasthttp 中有⼤量应⽤

https://github.com/valyala/fasthttp/blob/b433ecfcbda586cd6afb80f41ae45082959dfa91/server.go#L402

sync.Pool 发⽣ GC 时:

semaphore

是锁的实现基础,所有同步原语的基础设施。

sync.Mutex

sync.RWMutex

sync.Map

https://www.figma.com/proto/FMzUIdkjm4BEHSpwWFecew/concurrency?page-id=6%3A15&node-
id=6%3A16&viewport=-46%2C368%2C0.5078045725822449&scaling=min-zoom

sync.Waitgroup

Counter 减到 0 时,要唤醒所有 sema 上阻塞的 sudog

并发编程模式举例

CSP 和传统并发模式

Fan-in,合并多个 channel 操作

Or channel

任意 channel 返回
全部返回

Pipeline

串联在⼀起的 channel

并发同时保序

常⻅的并发 bug

死锁

RWR 死锁

循环等待死锁

注意:死锁问题需要通过 pprof 进⼊ goroutine ⻚⾯查看

Map concurrent writes/reads

崩溃时输出 stderr,请注意重定向你的 stderr 到单独的⽂件中

Channel 关闭 panic

Channel closing principle

    1. M receivers, one sender, the sender says “no more sends” by closing the data channel
    1. One receiver, N senders, the only receiver says “please stop sending more” by closing an additional signal channel
    1. M receivers, N senders, any one of them says “let’s end the game” by notifying a moderator to close an additional signal channel

go101.channel-closing

fn() 超时后,ch <- result 阻塞

goroutine永久泄露

wait group 使⽤不当,永久阻塞

context.WithCancel

内部启动 goroutine,在 ctx 被覆盖后泄露

死锁

闭包捕获本地变量

启动goroutine前要保证Add完成

并发操作 channel 时,多次关闭同⼀个 channel

Fn 耗时很久,但进⼊之前没有判断外部给的stopCh 中的通知浪费算⼒

内存模型

对于应⽤开发的同学来说,只要记住,使⽤显式同步就可以保证正确性。

现代计算机的多级存储结构
L1D cache ⼜会被划分为多个cache line,每个 cache line = 64 bytes

http://15418.courses.cs.cmu.edu/spring2015/lecture/basicarch/slide_042

L1 cache ⼜被划分为更细粒度的 cacheline,下⾯是在服务器上获取 L1 cache line size 的命令

1
2
$ getconf LEVEL1_DCACHE_LINESIZE
64

Runtime 中的 cacheline pad

http://15418.courses.cs.cmu.edu/spring2015/lecture/basicarch/slide_042

多核⼼给我们带来的问题:

  • 单变量的并发操作也必须⽤同步⼿段,⽐如 atomic
  • 全局视⻆下观察到的多变量读写的顺序可能会乱序

单变量的原⼦读/写,多核⼼使⽤ mesi 协议保证正确性

Mesi 协议是以整个 cache line 为单位进⾏的

https://www.scss.tcd.ie/Jeremy.Jones/VivioJS/caches/MESIHelp.htm

多核⼼执⾏时,CPU 和编译器可能对读写指令进⾏重排,使⽤ Litmus 测试观察内存重排:

检查两个核⼼的 EAX 是不是都为 0

https://github.com/herd/herdtools7

False sharing:因为 CPU 处理读写是以 cache line 为单位,所以在并发修改变量时,会⼀次性将其它 CPU core 中的cache line invalidate 掉,导致未修改的内存上相邻的变量也需要同步,带来额外的性能负担。

True sharing:多线程确实在共享并更新同⼀个变量/内存区域。

Happen-before 到底是什么?

同⼀个 goroutine 内的逻辑有依赖的语句执⾏,满⾜顺序关系。
编译器/CPU 可能对同⼀个 goroutine 中的语句执⾏进⾏打乱,以提⾼性能,但不能破坏其应⽤原有的逻辑。
不同的 goroutine 观察到的共享变量的修改顺序可能不⼀样。

初始化:
A pkg import B pkg,那么 B pkg 的 init 函数⼀定在 A pkg 的 init 函数之前执⾏。
Init 函数⼀定在 main.main 之前执⾏

Goroutine 创建:
Goroutine 的创建(creation)⼀定先于 goroutine 的执⾏(execution)

Goroutine 结束:
在没有显式同步的情况下,goroutine 的结束没有任何保证,可能被执⾏,也可能不被执⾏

Channel 收/发:
A send on a channel happens before the corresponding receive from that channel completes.

这⾥ c <- 0 ⼀定先于 <- c 执⾏完,所以 print ⼀定能打印出 hello world

The closing of a channel happens before a receive that returns a zero value because the channel is closed.

close(c) ⼀定先于 <-c 执⾏完,所以这⾥也可以保证打印出 hello world。

Channel 收/发:
A receive from an unbuffered channel happens before the send on that channel completes.

⽆ buffer 的 chan receive 先于 send 执⾏完,这⾥也可以保证打印出 hello world

Lock:For any sync.Mutex or sync.RWMutex variable l and n < m, call n of l.Unlock() happens before call m of l.Lock()returns

Unlock ⼀定先于 Lock 函数返回前执⾏完

Once:A single call of f() from once.Do(f) happens (returns) before any call of once.Do(f) returns.

本质是在⽤户不知道 memory barrier 概念和具体实现的前提下,能够按照官⽅提供的 happen-before 正确进⾏并发编程。

Memory barrier

在并发编程中的 memory barrier 和 GC 中的 barrier 不是⼀回事。
Memory barrier 是为了防⽌各种类型的读写重排:

⽽ GC 中的 read/write barrier 则是指堆上指针修改之前插⼊的⼀⼩段代码。

References

https://wudaijun.com/2018/02/go-sync-map-implement/
https://github.com/kat-co/concurrency-in-go-src
https://speakerdeck.com/kavya719/understanding-channels
https://www.zenlife.tk/concurrency-with-keep-order.md?hmsr=joyk.com&utm_source=joyk.com&utm_medium=referral
https://golang.org/ref/mem
https://www.hardwaretimes.com/difference-between-l1-l2-and-l3-cache-what-is-cpu-cache/
https://github.com/lotusirous/go-concurrency-patterns
https://songlh.github.io/paper/go-study.pdf
https://github.com/cch123/golang-notes/blob/master/memory_barrier.md

未涉及

  • 内置并发结构:sync.Cond
  • 进阶话题:如 acquire、release、sequential consistency。
  • Lock-Free,Wait-free 等等
  • 扩展并发原语:SingleFlight,ErrGroup 等
------ 本文结束------
如果本篇文章对你有帮助,可以给作者加个鸡腿~(*^__^*),感谢鼓励与支持!