sync

sync包实现了一些基础的同步原语;更高级的同步机制官方建议使用channel来实现;

同时包含atomic包,实现数据的原子操作;

以下原语对象在参数传递时,切忌不可被拷贝:XXX must not be copied after first use.

Mutex

锁是sync包的核心概念,其他原语的实现到多都是基于Mutex的封装,golang在Mutex之前抽象了Locker接口;

Mutex则是 Locker一种基本实现;

golang中Mutex是根据自旋锁实现,并在此基础上增加了优化策略来解决过度等待和饥饿问题;

自旋锁的一种简单表示(来自一个大佬)

自旋锁的基本描述中是需要基于atomic操作来保证排他性,不停的进行CAS尝试,成功也就表示锁成功,CAS操作的对象的值在0/1之间不停切换;

Mutex定义

相关概念

被锁状态(mutexLocked):锁处于锁住状态;

唤醒状态(mutexWoken状态):Unlock操作后唤醒某个goroutine,并发状态下,防止并发唤醒多个goroutine;

正常模式:等待队列中的goroutine根据FIFO的顺序依次竞争获取锁;此模式下,新加的goroutine在等待队列队列非空的情况下仍尝试获取锁(4次自旋尝试等待)获取到锁;

饥饿模式(mutexStarving状态):

触发条件是某个goroutine的等待时长超过1ms;

新加的goroutine也不会尝试去获取锁,不自旋等待;

Unlock操作直接交给等待队列的第一个goroutine;

这种模式是为了保证公平性,保证在队尾的goroutine也有可能获取到锁;

 

省略了部分并发检查的逻辑

这里不仅仅是对Lock状态的操作需要CAS,Mutex的所有状态更新都要保证CAS,如果CAS失败则要考虑Mutex状态已经被其他goroutine更新,代码中通过old = m.state来获取最新的状态

Lock

可对照源码阅读:src/sync/mutex.go

 

Unlock

总计一下,Mutex设计中几个要点:

1.新加goroutine首次获取锁失败放在队首,之后Lock失败则放入队尾(新增的goroutine正在CPU执行,把锁给他们会有很大的优势);

2.任何一个goroutine尝试获取锁的时长超过1ms,则进入饥饿模式;饥饿模式下,新加goroutine不会自旋等待,不会尝试获取锁;Unlock之后换新队首的goroutine;

RWMutex

读写锁,基于Mutex实现,如果只是使用Lock/Unlock,和Mutex是等效的;

实现上主要依赖的是Mutex和一些状态量,使得锁可以被任意数量的Reader或者单个Writer获取;

Lock:不仅要等待m.Lock(),还要判断readerCount是不是0;

RLock:只需要判断有没有Writer在等待(readerCount为特殊值);

这个很重要:只要有Writer被阻塞,新到的Reader也会被阻塞,直到Unlock;

Once

基于Mutex和一个标志字段done来实现,在Do()被调用一次之后done被置为1,之后不在触发f的执行;

WaitGroup

对于并发执行多个任务的场景,WaitGroup可用于等待所有任务执行结束;

Cond

Cond实现了类似广播/单播的同步场景;

广播:多个goroutine阻塞等待,广播导致这些goroutine都被唤醒;

单播:多个goroutine阻塞等待,单播导致这些goroutine中的某一个被唤醒(一般是FIFO);

####Pool

Pool实现了一个对象池,用于共享一些临时对象,避免频繁创建小对象给GC和内存带来压力;

结合bytes.Buffer更能实现共享的内存池,应对一般的高并发场景;

Map

并发安全的Map

空间换时间, 通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响

动态调整,miss次数多了之后,将dirty数据提升为read

优先从read读取、更新、删除,因为对read的读取不需要锁

TODO

sync/atomic

原子操作,具体实现主要在汇编代码;

使用LOCK汇编指令通过锁总线/MESI协议实现缓存刷新;

包括Load,Store,Add,Cas,Swap操作