long blogs

进一步有进一步惊喜


  • Home
  • Archive
  • Tags
  •  

© 2025 long

Theme Typography by Makito

Proudly published with Hexo

go-圣经-笔记

Posted at 2020-07-31 golang 《go语言圣经》 

基于共享变量的并发-笔记

一、竞争条件

  • 线性程序->顺序执行
  • 并发条件下,函数依然可以正确工作=>函数并发安全
  • 文档明确函数并发安全,才可以并发的访问它
并发无法工作

(1)死锁(deadlock)-> 干瞪眼,谁都不好过。
(2)活锁(livelock)->还活着,形成闭环,但没有什么进展。互相踢皮球,没有啥进展。
(3)饿死(resource starvation)->饭多,但是有些人没有分配到,永远处于等待,最后饿死。

数据竞争

两个以上的gorountine并发访问相同的变量,至少其中一个为写数据。
避免方法:
(1) 不要去写变量,在并发前完成初始化。
(2) 不允许多个gorountine同时访问变量,不要使用共享变量来通信,使用通信来共享数据。
(3) 互斥访问变量。

二、sync.Mutex互斥锁

  • 二元信号量:只能为0和1。
  • 临界区:在关锁和开锁之间形成读取安全的区域。
  • 可重入锁:go没有可重入锁。入口函数锁了,被调用的函数里再次请求锁失败造成阻塞。
  • 封装机制:在可导出的函数变量加锁,结构体不可导出可以不用加锁便可以被安全的访问。

三、sync.RWMutex读写锁

允许多个只读操作并发执行,但写操作会完全互斥。->“多读单写锁”(multiple reader,single writer)
go语言提供的多读单写锁sync.RWMutex

1
2
3
4
mu sync.RWMutex
mu.RLock()
defer mu.RUnlock()
return blance

blance变量可以被多个gorountine并发访问,但是写是互斥的。
**缺点:**RWMutex使用更加复杂的内部记录,比无竞争的Mutex慢一些。

四、内存同步

一个数据写入流程:cpu执行指令产生新数据->写入cpu缓存->写入主存。
不同的gorountine在读取共享变量的时候,都是从主存中读入数据。不加锁的变量在内存中的数据可能不一致。一个gorountine读取到的内存值可能不是另一个gorountine产生的最新数据。最新的数据还在另一个核中的缓存内,没有flush到主存中。
channel通信和互斥操作会使处理器将其缓存中的数据flush并commit。保证主存中的数据是最新的。

例子分析
1
2
3
4
5
6
7
8
9
var x,y int
go func(){
x = 1
fmt.Print("y:",y," ")
}()
go func(){
y = 1
fmt.Print("x:",x," ")
}()

数据的数据可能为:y:0 x:0
原因:
(1) 编译器优化:赋值和打印操作没有数据关联,编译器会让两条语句并发执行。先赋值还是先打印输出对于当前的gorountine执行结果是没有任何影响的。
(2) 缓存未更新:两个gorountine会运行在不同的核上,运行的最新结果(赋值的数据)还在cpu缓存中,未同步到主存里面。这时候另外一个gorountine从主存中获得的数据还是旧的数据

五、sync.Once惰性初始化

需求:
(1)需要相关变量的时候才去初始化。
(2)只初始化一次
问题:
当一个gorountine发现数据未初始化时,调用初始化函数。在该协程初始化的过程中,另外一个gorountine尝试获取数据,发现数据还没初始化,又一次的调用初始化函数。
解决办法:
(1)加互斥锁,保证初始化成功,不支持并发读。
(2)读取数据部分使用RWMuntex支持并发读,初始化使用互斥锁。读得到就读,读不到就只允许一个调用初始化函数。
(3)使用sync.Once->Do函数只会调用一次

例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

var mu sync.Once
var wg sync.WaitGroup
var config map[string]int
func initConfig(){
fmt.Println("call initConfig function")
// 加长初始化的时间
time.Sleep(1*time.Second)
config = map[string]int{
"hello":1,
"world":2,
}
}
// 获得配置,
// 如果配置没有初始化,就调用初始化函数
func getConfigure(key string) int {
v,ok := config[key]
if !ok {
initConfig()
v = config[key]
}
return v
}

func main() {
// 只初始化一次
mu.Do(initConfig)
wg.Add(1)
go func() {
fmt.Println(getConfigure("hello"))
wg.Done()
}()
wg.Add(1)
go func() {
fmt.Println(getConfigure("world"))
wg.Done()
}()

wg.Wait()
}

六、竞争条件检测

go build/run/test -race生成报告文档

gorountine和线程

  • 上下文切换不同
  • 调度器不同
  • 动态栈

Share 

 Previous post: git代码提交规范 Next post: go-gorm 

© 2025 long

Theme Typography by Makito

Proudly published with Hexo