前言
本专栏是笔者在学习《Go程序设计语言》这本书时,对每个章节认为较为重要(容易忘记👻)的知识点记录的笔记,其中也会有少量的思考👀, 现整理成博客分享出来。
如果对专栏感兴趣,跑过去看一眼,书中的每一章都有:《Go程序设计语言》笔记
❗️注意❗️:本专栏不是详细的知识讲解,只是碎片的知识条目,或可作为Go知识点查漏补缺的小工具~
竞态
-
如果无法肯定地说一个事件必定先于另一个事件,那这两个事件就是并发的;
-
一个能在串行程序中正确工作的函数,如果并发调用时仍能正常工作,则该函数是**并发安全(concurrency-safe)**的;
-
要回避并发访问,要么变量只存在在一个
goroutine
内,要么维护一个更高层的互斥不变量; -
竞态是指多个
goroutine
按某些交错顺序进行时,无法给出正确的结果;竞态很难再现和分析; -
数据竞态发生于两个
goroutine
并发读取某个数据,且至少一个写入数据; -
避免数据竞态的三种办法:
- 数据初始化后不再写入;
- 避免从多个
goroutine
中访问同一个变量; - 互斥机制:同一时间只允许一个
goroutine
访问变量;
-
监控goroutine:使用通道请求来代理一个受限变量的所有访问,这个
goroutine
就是可以称为变量的goroutine
;-
比如存取余额时:
-
package bank // 将存钱和取钱的数值,都通过通道传递 var deposits = make(chan int) var balances = make(chan int) func Deposit(amount int){ deposits<-amount} func Balance()int{return balances } func teller(){ // 在goroutine中处理通道传递的数值,实现代理 var balance int for{ // select保证同一时刻只发生一个 select{ case amount := <-deposits: balance += amount case balances <- balance: } } } func init(){ // 初始化时打开balance变量的监控goroutine go teller() }
-
-
不要通过共享内存来通信,而是通过通信来共享内存;
互斥锁:sync.Mutex
- 可以使用一个容量为1的通道来保证同一时间最多有
1
个goroutine
能访问共享变量;sync.Mutex
实现了该机制;- 使用
mu.Lock()
获取令牌; - 使用
mu.Unlock()
释放令牌; Lock()
和Unlock()
之间的部分被称为临界区;- 当函数结构比较复杂时,可以使用
defer mu.Unlock()
来确保释放锁,但执行成本会增大一些,后续可以优化;
- 使用
读写互斥锁:sync.RWMutex
- 多读单写锁:只读操作可以并发执行,但写操作需要独享变量;
sync.RWMutex
实现了多读单写锁,使用mu.RLock()、mu.RUnlock()
来获取、释放读锁;- 多读单写锁内部实现更复杂,所以仅在竞争较为激烈(每次都需要等待才能获取锁)时才使用;竞争不激烈时使用
sync.Mutex
即可,效率更高;
内存同步
-
并不只是自己操作会被打断时才加锁,也有可能是为避免打断他人操作才加锁;
-
同步不仅涉及多个
goroutine
的执行顺序,还会影响到内存: -
考虑下面的程序,有可能输出是:
x=0 y=0
,原因是有可能内存还没被同步;var x int var y int go func(){ x = 1 fmt.Print("y=", y) } go func(){ y = 1 fmt.Print("x=", x) }
-
并发问题最好从机制上避免:尽可能将变量限制在一个
goroutine
内;其它情况使用互斥锁;
延迟初始化sync.Once
-
预先初始化一个变量会增加程序的启动延时;
-
sync.Once
概念上包含一个bool
和一个互斥量,bool
检测是否初始化成功,互斥量保护这个bool
和客户端的数据结构;它包含唯一方法sync.Once.Do()
,传入一个初始化函数;-
实际实现:
sync.Once
: -
type Once struct{ done uint32 m Mutex } func (o *Once) Do(f func()){...} func (o *Once) doSlow(){...}
-
-
sync.Once
可以在只需要初始化一次的场合,使用简单的逻辑来避免重复初始化;
竞态检测器
- 在
go run
、go build
、go test
命令中使用-race
即可使用go
的竞态检测器(race detector
); - 竞态检测器会研究事件流,找到那些有问题的案例,报告出检测到的数据竞态;
- 它只能检测运行时发生了的竞态,而不能保证不会发生竞态;
示例:并发非阻塞缓存
goroutine与线程
- 一个
goroutine
在生命周期开始时只有一个很小的栈,典型情况下为2KB
;用来存放那些正在执行或临时暂停的函数中的局部变量; - 栈可增长至
1GB
; OS
线程由内核每隔几毫秒调度;go
运行时还有一个自己的调度器:采用m:n
调度技术,可以复用m
个goroutine
到n
个OS
线程;go
调度器不由硬件时钟触发,而是由语言结构(如time.Sleep
)触发调度;所以goroutine
的调度比线程调度的成本低很多;go
调度器使用GOMAXPROCS
参数来确定使用多少个OS
线程来同时执行go
代码;默认为CPU
数量;goroutine
没有标识,线程一般是有标识的;
如有错误 ❌ ,欢迎指正 ☝️~
如有收获 🍗,可以考虑点赞👍/评论💬/收藏⭐️/关注👀,大家共同进步~