go并发编程
Mutex几种状态
- mutexLocked 表示互斥锁的锁定状态
- mutexWoken 唤醒锁
- mutexStarving 当前互斥锁进入饥饿状态
- mutexWaiterShift 统计阻塞在这个互斥锁上的goroutine的数目
互斥锁无冲突是最简单的情况了,有冲突时,首先进行自旋,因为Mutex保护的代码段都很短
经过短暂的自旋就可以获得,如果自旋等待无果,就只好通过信号量让当前goroutine进入Gwaitting状态
Mutex的正常模式和饥饿模式
- 正常模式(非公平锁)
- 正常模式下,所有等待的goroutine按照FIFO(先进先出)的顺序等待
- 唤醒的goroutine不会直接拥有锁而是会和新进来的请求goroutine竞争锁
- 新请求的goroutine更容易抢占,因为它正在CPU上执行,所以刚刚唤醒的goroutine很大可能会竞争失败
- 在这种情况下这个被唤醒的goroutine会被加入到等待队列的前面
- 饥饿模式(公平锁)
- 为了解决等待goroutine队列的长尾问题,饥饿模式下
- 直接由unlock把锁交给等待队列中第一个goroutine(队头),同时,饥饿模式下
- 新进来的goroutine不会进行抢锁,也不会进入自旋状态,会直接进入等待队列的尾部
- 这样很好的解决了老的goroutine一直抢不到锁的情况
- 饥饿模式的触发条件
- 当一个goroutine等待时间超过1毫秒时,或者当前队列剩下一个goroutine的时候,Mutex切换到饥饿模式
- 总结
- 对于两种模式,正常模式下性能是最好的,因为goroutine可以连续多次获取锁
- 饥饿模式解决了取锁公平的问题,但是性能会下降,这其实是性能和公平的一个平衡模式
Mutex允许自旋的条件
- 锁已被占用,且锁不处于饥饿模式
- 积累的自旋次数小于最大自旋次数
- CPU核数>1
- 有空闲的P
- 当前的goroutine所挂载的P下,本地待运行队列为空
RWMutex实现
- 通过记录readerCount读锁的数量来进行限制,当有一个写锁的时候,会将读锁的数量设置为负数1<<30
- 目的是让新进来的读锁等待之前的写锁释放通知读锁
- 同样当有写锁进行抢占时,也会等待之前的读锁都释放完毕,才进行后续的操作
- 而等写锁释放完成之后,会将值重新加上1<<30,并通知刚才新进入的读锁(rw.readerSem)两者互相限制
RWMutex注意事项
- RWMutex是个单写多读锁,可以加多个读锁或者一个写锁
- 读锁占用的情况下会阻止写,不会阻止读,多个goroutine可以同时获取读锁
- 写锁会阻止其它goroutine(无论读写锁)进来,整个锁由该goroutine独占
- 适用于读多写少的场景
- RWMutex的零值是一个未锁定状态的互斥锁
- RWMutex在首次使用之后就不能再被拷贝
- RWMutex的读锁或写锁在未锁定的状态下进行解锁都会引发panic
- RWMutex的一个写锁去锁定临界区的共享资源,如果临界区的资源已被(读锁或写锁)锁定,这个写锁的goroutine会被阻塞直到解锁
- RWMutex的读锁不要用于递归调用,容易产生死锁
- RWMutex的锁定状态与特定的goroutine没有关联,一个goroutine可以RLock(Lock),另外一个goroutine可以RUnlock(Unlock)
- 写锁被解锁后,所有因操作锁定读锁的goroutine会被唤醒,并都可以成功锁定读锁
- 读锁被解锁后,在没有其它读锁锁定的情况下,所有因操作锁定写锁而被阻塞的goroutine中,其中等待时间最长的goroutine会被唤醒
cond是什么
- Cond实现了一种条件变量,可以使用在多个reader等待共享资源ready的场景,(如果只有一读一写,一个锁或者channel就搞定了)
- 每个Cond都会关联一个Lock(*sync.Mutex or *sync.RWMutex), 当修改条件或者调用Wait方法时,必须加锁以保护condition
- 案例:
var (
c = sync.Cond{L: &sync.Mutex{}}
maxNum = 15
)
func main(){
for i := 0; i < maxNum; i++{
go func(i int) {
c.L.Lock()
defer c.L.Unlock()
c.Wait()
fmt.Println("goroutine:", i)
time.Sleep(time.Millisecond * 100)
}(i)
}
c.L.Lock()
maxNum = 16
c.L.Unlock()
c.Broadcast()
time.Sleep(time.Second * 2)
}
Broadcast和Signal的区别
- Broadcast会唤醒所有等待c的goroutine,调用Broadcast的时候,可以加锁也可以不加锁
- Signal只唤醒一个等待c的goroutine,调用Signal的时候,可以加锁也可以不加锁
Cond中Wait使用
- Wait会自动释放c.L锁,并挂起调用者的goroutine,之后恢复执行
- Wait会在返回时对c.L加锁
- 除非被Broadcast或Signal唤醒,否则Wait不会返回
- 由于Wait第一次恢复是,c.L并没有加锁,所以当Wait返回时,调用者通常不能假设条件为真
- 简单来说,只要想使用condition就必须加锁
WaitGroup用法
- 一个WaitGroup对象可以等待一组协程结束,使用方法是
- main协程通过调用 wg.add(delta int) 来设置worker协程的个数,然后创建worker协程
- worker协程结束以后,都要调用wg.Done()
- main协程调用wg.Wait()而被block, 知道所有worker协程全部执行结束后返回
- 如果不确定要创建的worker协程数量,就不要一次性wg.add(),而是在每个创建worker协程之前调用一次wg.add(1)
- 首次使用后不得复制wg
WaitGroup实现原理
- WaitGroup主要维护了两个计数器,一个请求计数器v,一个等待计数器w
- 二者组成了一个64bit的值,请求计数器占高32bit,等待计数器占低32bit
- 每次Add执行,请求计数器 v+1, 每次Done,等待计数器 w-1
- 当v为0时,通过信号量唤醒Wait
什么事sync.Once
- Once可以用来执行且仅仅执行一次的动作,常常用于单例对象的初始化场景
- Once常常用来初始化单例资源,或者并发访问只需要初始化一次的共享资源
- 或者在测试的时候初始化一次测试资源
- sync.Once只暴露了一个方法Do,你可以多次调用Do方法
- 但是只有第一次调用Do方法时f参数才会执行,这里的f是一个无参数无返回值的函数
什么操作叫原子操作
- 原子操作即是进程过程中不能被中断的操作,针对某个值的原子操作在被进行的过程中
- CPU绝不会再去进行其它针对值的操作,为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成
- 原子操作是无锁的,常常直接通过CPU指令直接实现,
- 事实上,其它同步技术的实现常常依赖于原子操作
原子操作和锁的区别
- 原子操作由底层硬件支持,而锁由操作系统调度器实现
- 锁应当用来保护一段逻辑,对于一个变量更新的保护
- 原子操作通常执行上会更有效率,并且更能利用计算机多核资源
- 如果要更新的是一个复合对象,则应当使用automic.Value封装好的实现
sync.Pool有什么用
- 对于很多需要重复分配、回收内存的地方,sync.Pool是一个很好的选择
- 频繁的分配回收内存会给GC带来一定负担,严重的时候会引起CPU的毛刺
- 而sync.Pool可以将暂时不使用的对象缓存起来,待下次需要的时候直接使用
- 不用再次经过内存分配,复用对象的内存,减轻GC的压力,提升系统性能
- 案例
func main(){
var p sync.Pool
var a = "jdfakljfdalfaskdj阿加加加金灯送福卡萨发几块爱神的箭快疯了金阿奎"
p.Put(a)
ret := p.Get()
fmt.Println(ret)
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)