🤨 一、什么是自旋锁
1、自旋锁提出的背景
在互斥地访问临界资源时,需要引入锁控制临界区,只有获取了锁的线程才能够对资源进行访问,同一时刻只能有一个线程获取到锁。那么没有获取到锁的线程应该怎么办?
自旋锁互斥锁
简而言之,需要获得自旋锁的线程循环等待,判断是否获得锁;而需要获得互斥锁的线程阻塞自己,等待其它线程解锁。
2、自旋锁的作用
自旋锁适合锁资源在短时间内获取/释放的场景。当锁资源状态会在短时间内切换时,共享锁定的线程就避免了进入阻塞状态,从而降低了用户进程和内核切换的消耗。
🚀 二、自旋锁的Go实现
ants
antsworkersworkers
// 自旋锁的使用
p.lock.Lock()
// 获取worker队列中过期的worker
expiredWorkers := p.workers.retrieveExpiry(p.options.ExpiryDuration)
p.lock.Unlock()
(2)自旋锁的定义
type spinLock uint32
const maxBackoff = 16
func (sl *spinLock) Lock() {
backoff := 1
for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
// 指数退避算法
for i := 0; i < backoff; i++ {
runtime.Gosched()
// runtime.Gosched(),用于让出CPU时间片,让出当前goroutine的执行权限,
// 调度器安排其它等待的任务运行,并在下次某个时候从该位置恢复执行。
}
if backoff < maxBackoff {
backoff <<= 1
}
}
}
func (sl *spinLock) Unlock() {
atomic.StoreUint32((*uint32)(sl), 0)
}
// NewSpinLock instantiates a spin-lock.
func NewSpinLock() sync.Locker {
return new(spinLock)
}
(3)小结
antsCAS自旋ants指数退避算法
2、实现自旋锁的原理
antsatomic.CompareAndSwapXXXCAS自旋
atomic.CompareAndSwapXXX
atomic.CompareAndSwapXXXatomictruefalsegoroutine
func main() {
var v int32
var old int32 = 999
var new_ int32 = 666
v = old
is := atomic.CompareAndSwapInt32(&v, old, new_)
if is {
fmt.Println("交换成功!v=", v)
} else {
fmt.Println("交换失败!v=", v)
}
}
// [out] 交换成功!v= 666
func main() {
var v int32
var old int32 = 999
var new_ int32 = 666
v = old
is := atomic.CompareAndSwapInt32(&v, 111, new_)
if is {
fmt.Println("交换成功!v=", v)
} else {
fmt.Println("交换失败!v=", v)
}
}
// 交换失败!v= 999
3、具体实现
type SpinLock uint32
func (s *SpinLock) Lock() {
// 当s=0时(Unlock),尝试设置为1(Lock)
for !atomic.CompareAndSwapUint32((*uint32)(s), 0, 1) {
runtime.Gosched() // 让出当前G的执行权
}
}
func (s *SpinLock) Unlock() {
atomic.StoreUint32((*uint32)(s), 0)
}
func NewSpinLock() sync.Locker {
var l SpinLock
return &l
}
🚦 三、对比测试
下面通过多个G竞争自旋锁和互斥锁,对比程序的性能。
1、场景
goroutine
func TLock(l sync.Locker) {
var wg sync.WaitGroup
N := 5 // N个G竞争锁
for i := 0; i < N; i++ {
wg.Add(1)
go func() {
defer wg.Done()
l.Lock()
time.Sleep(time.Nanosecond * 100) // 模拟数据的读取操作
l.Unlock()
// 对读取到的数据的操作
}()
}
wg.Wait()
}
2、测试函数
func BenchmarkSpinLock(b *testing.B) {
for i := 0; i < b.N; i++ {
l := NewSpinLock()
TLock(l)
}
b.ReportAllocs()
}
func BenchmarkMutex(b *testing.B) {
for i := 0; i < b.N; i++ {
var l sync.Mutex
TLock(&l)
}
b.ReportAllocs()
}
3、结果
>> go test -bench . -benchtime=60s
goos: windows
goarch: amd64
pkg: demo
cpu: Intel(R) Core(TM) i5-10210U CPU @ 1.60GHz
BenchmarkSpinLock-8 1418 46357656 ns/op 625 B/op 12 allocs/op
BenchmarkMutex-8 978 74129670 ns/op 609 B/op 12 allocs/op
PASS
ok demo 151.029s
(1)可以看到,在本地笔记本的规格上,对于小区域、短时间占用的临界区,使用自旋锁的效率大约是是使用互斥锁的1.6倍。
- 注意,在不同架构或者不同CPU主频的机器上测试结果可能存在差异,CPU主频越高的机器上,自旋锁与互斥锁的差异越小(我的笔记本为 1.60 GHZ,算是低的了,因此差异较为明显)。
⏰四、小结
1、自旋锁的优点
(1)自旋锁主要是为了降低了用户进程和内核切换的消耗,适用于等待获得锁时间较短的场景。
2、自旋锁的缺点
(1)当锁轮转时间较长,单个线程占用锁的时间较长时,自旋锁循环等待的时间较长,会消耗大量的CPU资源。
TicketLock
(3)是否决定使用自旋锁需要综合考虑实际的场景和生产环境。