一: 原子操作 CAS(compare-and-swap)
原子操作分三步: 读取 addr 的值, 和 old 进行比较, 如果相等, 则将 new 赋值给 * addr, 他能保证这三步一起执行完成, 叫原子操作也就是说它不能再分了, 当有一个 CPU 在访问这块内容 addr 时, 其他 CPU 就不能访问
- func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
- TEXT .CompareAndSwapUint64(SB),NOSPLIT,$0-25
- MOVD addr+0(FP), R3
- MOVD old+8(FP), R4
- MOVD new+16(FP), R5
- SYNC
- LDAR (R3), R6
- CMP R6, R4
- BNE 7(PC)
- STDCCC R5, (R3)
- BNE -4(PC)
- ISYNC
- MOVD $1, R3
- MOVB R3, swapped+24(FP)
- RET
- MOVB R0, swapped+24(FP)
- RET
二: 普通锁
加锁(Mutex.Lock)
1: 原子操作加锁: 原子操作判断是否已经被加锁, 如果没有加锁, 原子操作加锁, 直接返回, 很快吗!
2: 执行旋转锁: 已经被加锁, 判断是否可以执行旋转锁, 执行旋转锁, 原子判断是否可以加锁, 若可以, 加锁返回
3: 当前 G 休眠等待被唤醒: 在执行旋转锁期间, 锁还是没释放, 那就只能让当前协程休眠, 等待被唤醒, 当锁被释放后, 当前 G 被唤醒继续执行
释放锁(Mutex.UnLock)
1: 将加锁状态去掉, 判断是否有等待的协程, 如没有直接返回
2: 若有等待协程, 将状态设置成唤醒状态
3: 唤醒一个等待协程
三: 读写锁
读写锁基于普通锁实现
加写锁(RWMutex.Lock)
1: 加普通锁
2: 改读锁的数量 readerCount -= 1 <<30
3: 如果有正在读的锁, 等待直到读锁完成, 读写不能同时进行
释放写锁(RWMutex.UnLock)
1: 改读锁的数量 readerCount += 1 << 30, 加锁的时候减了这么多, 释放锁的时候加回来
2: 如果 readerCount>= 1 << 30, 抛异常, 释放没有加锁的锁
3: 唤醒所有正在等待读的协程
4: 释放普通锁
加读锁(RWMutex.RLock)
1: 原子操作读锁数量加 1,readerCount+=1
2: 如果 rederCount<0, 说明有写功能正在执行, 协程进入睡眠状态, 等待写完之后被唤醒
3: 如果没有正在执行的写锁, 就完事了, 整个加锁操作就只执行了一个原子操作, 还是很快的
释放读锁(RWMutex.RUnLock)
1: 原子操作读锁数量减 1
2: 如果读锁数量 ==-1, 或 ==-1 << 30, 说明释放了一个没有加读锁的锁, 或者释放了一个正在写的锁, 直接报错
3: 如果有正在等待的写锁, 唤醒它, 否则整个释放读锁也就执行了一个原子操作
所以说, 锁是基于原子操作的, 原子操作保证了数据的一致性, 读写锁基于普通锁来实现, 对于一个写少读多的程序来说, 读写锁会比普通锁快很多
加锁原理
1: 先是 CAS 的方式尝试获取锁, 如果获取到了, 就锁住, 并继续执行被锁住的代码, 然后在释放锁
2:CAS 没有拿到锁, 就只能等待了, 比如有 10 个协程 (G) 在等这个待锁, go 并不是一把锁创建一个队列, 而是默认创建 251 个队列, 通过 hash 的方式将 G 加入队列, 确保等待同一把锁的 G 在同一个队列, 然后将当前 G 执行上下文信息保存到 G.sched, 下次就可以继续从这里执行, 这样这个等待的 G 就这样被扔到队列中了, 而不是将这个 G 状态改成等待状态等待被唤醒, G 去睡觉了, P 还得继续执行, 于是会找一个 P, 继续执行
解锁原理
1: 通过锁定位到对应的队列, 所有等待这把锁的 G 都在这个队列中, 查找是否有等待的 G, 没有就返回
2: 有就将 G 状态改成可运行, 并加入到运行队列, 等待被调度
关于 G 调度请看我的这篇文章: go 并发调度原理学习
来源: https://www.cnblogs.com/hlxs/p/10281017.html