Go并发编程-极客时间专栏学习总结
前言
任务编排用Channel,共享资源保护用传统并发原语
“针对同一种场景,也许存在很多并发原语都适用的情况,但是一定是有最合适的那一个。 所以,你必须非常清楚每种并发原语的实现机制和适用场景,千万不要被网上的一些文章误导,万事皆用 Channel。”
summary
- 并发编程,为什么选Go?
“一句话,我被 Go 的简单高效所打动。它不仅部署方便,自带完善的工具链, 特别是 Go 在处理并发场景上表现出的独特性能,更是让我着迷。”
“Go 并发编程的重要性不容置疑。只要是使用 Go 开发的大型应用程序,并发是必然要采用的技术。”
- 怎么提升 Go 并发编程能力?
知识主线:
基本并发原语–>原子操作–>Channel–>扩展并发原语–>分布式并发原语
学习主线:
基本用法–>实现原理–>易错场景–>知名项目中的Bug
课程模块
基本并发原语:
介绍 Mutex、RWMutex、Waitgroup、Cond、Pool、Context 等标准库中的并发原语, 这些都是传统的并发原语,在其它语言中也很常见,是我们在并发编程中常用的类型;
原子操作:
介绍Go 标准库中提供的原子操作。原子操作是其它并发原语的基础,学会了就可以自己创造新的并发原语;
Channel:
Channel 类型是 Go 语言独特的类型,因为比较新,所以难以掌握。 基本用法,处理场景和应用模式,避免踩坑;
扩展并发原语:
目前来看,Go 开发组不准备在标准库中扩充并发原语了,但是还有一些并发原语应用广泛, 比如信号量、SingleFlight、循环栅栏、ErrGroup 等。 掌握了它们,就可以在处理一些并发问题时,取得事半功倍的效果;
分布式并发原语:
分布式并发原语是应对大规模的应用程序中并发问题的并发类型。 介绍使用 etcd 实现的一些分布式并发原语,比如 Leader 选举、分布式互斥锁、分布式读写锁、分布式队列等, 在处理分布式场景的并发问题时,特别有用;
refs
https://time.geekbang.org/column/article/294849
https://myslide.cn/slides/23014#
2020.10.13更新
基本并发源语
1.Mutex:如何解决资源并发访问问题
go并发
import (
"fmt"
"sync"
)
func main() {
var count = 0
// 使用WaitGroup等待10个goroutine完成
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
// 对变量count执行10次加1
for j := 0; j < 100000; j++ {
count++
}
}()
}
// 等待10个goroutine完成
wg.Wait()
fmt.Println(count)
}
// 310909
// 224339
// 198934
count++ 不是一个原子操作,它至少包含几个步骤,比如读取变量 count 的当前值,对这个值加 1,把结果再保存到 count 中。因为不是原子操作,就可能有并发的问题。
// count++操作的汇编代码
MOVQ "".count(SB), AX
LEAQ 1(AX), CX
MOVQ CX, "".count(SB)
Go race detector
Go 提供了一个检测并发访问共享资源是否有问题的工具: race detector,它可以帮助我们自动发现程序有没有 data race 的问题。
Go race detector 是基于 Google 的 C/C++ sanitizers 技术实现的,编译器通过探测所有的内存访问,加入代码能监视对这些内存地址的访问(读还是写)。在代码运行的时候,race detector 就能监控到对共享变量的非同步访问,出现 race 的时候,就会打印出警告信息。
Go 1.1 中就引入了这种技术,并且一下子就发现了标准库中的 42 个并发问题。现在,race detector 已经成了 Go 持续集成过程中的一部分。
go run -race count.go
qwq go_practice % go run -race count.go
==================
WARNING: DATA RACE
Read at 0x00c0000ce008 by goroutine 8:
go_practice/go_concurrency.Count.func1()
/root/GO/src/go_practice/go_concurrency/1.go:22 +0x75
Previous write at 0x00c0000ce008 by goroutine 7:
go_practice/go_concurrency.Count.func1()
/root/GO/src/go_practice/go_concurrency/1.go:22 +0x8b
Goroutine 8 (running) created at:
go_practice/go_concurrency.Count()
/root/GO/src/go_practice/go_concurrency/1.go:18 +0xe4
main.main()
/root/GO/src/go_practice/count.go:6 +0x2f
Goroutine 7 (running) created at:
go_practice/go_concurrency.Count()
/root/GO/src/go_practice/go_concurrency/1.go:18 +0xe4
main.main()
/root/GO/src/go_practice/count.go:6 +0x2f
==================
216910
Found 1 data race(s)
exit status 66
Go race detector只能通过真正对实际地址进行读写访问的时候才能探测, 所以它并不能在编译的时候发现 data race 的问题
Mutex
package main
import (
"fmt"
"sync"
)
func main() {
// 互斥锁保护计数器
var mu sync.Mutex
// 计数器的值
var count = 0
// 辅助变量,用来确认所有的goroutine都完成
var wg sync.WaitGroup
wg.Add(10)
// 启动10个gourontine
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
// 累加10万次
for j := 0; j < 100000; j++ {
mu.Lock()
count++
mu.Unlock()
}
}()
}
wg.Wait()
fmt.Println(count)
}
使用
很多情况下,Mutex 会嵌入到其它 struct 中使用;
可以采用嵌入字段的方式。通过嵌入字段,你可以在这个 struct 上直接调用 Lock/Unlock 方法。
func main() {
var counter Counter
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
for j := 0; j < 100000; j++ {
counter.Lock()
counter.Count++
counter.Unlock()
}
}()
}
wg.Wait()
fmt.Println(counter.Count)
}
type Counter struct {
sync.Mutex
Count uint64
}
还可以把获取锁、释放锁、计数加一的逻辑封装成一个方法,对外不需要暴露锁等逻辑
func main() {
// 封装好的计数器
var counter Counter
var wg sync.WaitGroup
wg.Add(10)
// 启动10个goroutine
for i := 0; i < 10; i++ {
go func() {
defer wg.Done()
// 执行10万次累加
for j := 0; j < 100000; j++ {
counter.Incr() // 受到锁保护的方法
}
}()
}
wg.Wait()
fmt.Println(counter.Count())
}
// 线程安全的计数器类型
type Counter struct {
CounterType int
Name string
mu sync.Mutex
count uint64
}
// 加1的方法,内部使用互斥锁保护
func (c *Counter) Incr() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
// 得到计数器的值,也需要锁保护
func (c *Counter) Count() uint64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
a static race detector
https://github.com/amit-davidson/Chronos
go get -v github.com/amit-davidson/Chronos/cmd/chronos
chronos --file <path_to_main>
help
Usage of ./chronos:
-file string
The file containing the entry point of the program
-pkg string
Path to the to pkg of the file. Tells Chronos where to perform the search. By default, it assumes the file is inside $GOPATH
Support:
Detects races on pointers passed around the program.
Analysis of conditional branches, nested functions, interfaces, select, gotos, defers, for loops and recursions.
Synchronization using mutex and goroutines starts.
Limitations:
Big programs and external packages. (Due to stack overflow)
Synchronization using channels, waitgroups, once, cond and atomic.
refs
https://github.com/google/sanitizers
https://blog.golang.org/race-detector
https://github.com/moby/moby/pull/37583
https://github.com/moby/moby/pull/35517
https://github.com/moby/moby/pull/32826
https://github.com/moby/moby/pull/30696
https://github.com/kubernetes/kubernetes/pull/72361
https://github.com/kubernetes/kubernetes/pull/71617
https://colobu.com/2018/12/18/dive-into-sync-mutex/
https://studygolang.com/articles/17017
https://golang.org/src/sync/mutex.go
2020.11.10更新
2.Mutex:庖丁解牛看实现
3.Mutex:4种易错场景
4.Mutex:如何扩展额外功能
5.RWMutex:读写锁的实现原理及避坑指南
6.WaitGroup:协同等待,任务编排利器
7.Cond:条件变量的实现机制及避坑指南
8.Once:一个简约而不简单的同步原语
9.Map:如何实现线程安全的Map类型
10.Pool:性能提升大杀器
11.Context:信息穿透上下文
原子操作
12.atomic:要保证原子操作,一定要使用这几种方法
Channel
13.Channel:另辟蹊径,解决并发问题
14.Channel:透过代码看典型的应用模式
15.内存模型:Go如何保证并发读写的顺序?
扩展并发原语
16.Samaphore:一篇文章搞懂信号量
17.SingleFlight和CyclicBarrier:请求合并和循环栅栏
18.分组操作:处理一组子任务,该用什么并发原语
分布式并发原语
19.在分布式环境中,Leader选举和互斥锁该如何实现?
20.在分布式环境中,队列、STM该如何实现?
结束语
refs
2020 新春流行的RPC框架性能大比拼: https://colobu.com/2020/01/21/benchmark-2019-spring-of-popular-rpc-frameworks/
鸟窝: https://colobu.com/
https://www.youtube.com/watch?v=f6kdp27TYZs
https://github.com/golang/go/issues/5045