基于共享变量的并发-笔记
一、竞争条件
- 线性程序->顺序执行
- 并发条件下,函数依然可以正确工作=>函数并发安全
- 文档明确函数并发安全,才可以并发的访问它
并发无法工作
(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 | mu sync.RWMutex |
blance
变量可以被多个gorountine并发访问,但是写是互斥的。
**缺点:**RWMutex使用更加复杂的内部记录,比无竞争的Mutex慢一些。
四、内存同步
一个数据写入流程:cpu执行指令产生新数据->写入cpu缓存->写入主存。
不同的gorountine在读取共享变量的时候,都是从主存中读入数据。不加锁的变量在内存中的数据可能不一致。一个gorountine读取到的内存值可能不是另一个gorountine产生的最新数据。最新的数据还在另一个核中的缓存内,没有flush到主存中。
channel通信和互斥操作会使处理器将其缓存中的数据flush并commit。保证主存中的数据是最新的。
例子分析
1 | var x,y int |
数据的数据可能为:y:0 x:0
原因:
(1) 编译器优化:赋值和打印操作没有数据关联,编译器会让两条语句并发执行。先赋值还是先打印输出对于当前的gorountine执行结果是没有任何影响的。
(2) 缓存未更新:两个gorountine会运行在不同的核上,运行的最新结果(赋值的数据)还在cpu缓存中,未同步到主存里面。这时候另外一个gorountine从主存中获得的数据还是旧的数据
五、sync.Once惰性初始化
需求:
(1)需要相关变量的时候才去初始化。
(2)只初始化一次
问题:
当一个gorountine发现数据未初始化时,调用初始化函数。在该协程初始化的过程中,另外一个gorountine尝试获取数据,发现数据还没初始化,又一次的调用初始化函数。
解决办法:
(1)加互斥锁,保证初始化成功,不支持并发读。
(2)读取数据部分使用RWMuntex支持并发读,初始化使用互斥锁。读得到就读,读不到就只允许一个调用初始化函数。
(3)使用sync.Once->Do函数只会调用一次
例子
1 |
|
六、竞争条件检测
go build/run/test -race
生成报告文档
gorountine和线程
- 上下文切换不同
- 调度器不同
- 动态栈