• 【Go并发】自旋锁及Go实现


    本文是ants源码解析系列的引申阅读,阐释了什么是自旋锁、自旋锁的作用、Go实现以及测试对比。

    1、参考文章

    看完你就明白的锁系列之自旋锁 - 程序员cxuan - 博客园 (cnblogs.com)

    CAS自旋_iqqcode的博客-CSDN博客_cas自旋

    golang 自旋锁 - 简书 (jianshu.com)

    2、变更说明

    (1)2022年08月28日发布第一版

    🤨 一、什么是自旋锁
    1、自旋锁提出的背景

    在互斥地访问临界资源时,需要引入锁控制临界区,只有获取了锁的线程才能够对资源进行访问,同一时刻只能有一个线程获取到锁。那么没有获取到锁的线程应该怎么办?

    通常有两种处理方式:一种是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,这种锁叫做**自旋锁**,它不用将线程阻塞起来(NON-BLOCKING);还有一种处理方式就是把自己阻塞起来,等待重新调度请求,这种叫做互斥锁

    简而言之,需要获得自旋锁的线程循环等待,判断是否获得锁;而需要获得互斥锁的线程阻塞自己,等待其它线程解锁。

    2、自旋锁的作用

    ​ 自旋锁适合锁资源在短时间内获取/释放的场景。当锁资源状态会在短时间内切换时,共享锁定的线程就避免了进入阻塞状态,从而降低了用户进程和内核切换的消耗。


    🚀 二、自旋锁的Go实现
    1、参考ants源码

    (1)在ants源码中,使用自旋锁控制访问池的workers队列,获取其过期的workers

    // 自旋锁的使用
    p.lock.Lock()
    // 获取worker队列中过期的worker
    expiredWorkers := p.workers.retrieveExpiry(p.options.ExpiryDuration)
    p.lock.Unlock()
    
    • 1
    • 2
    • 3
    • 4
    • 5

    (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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    (3)小结

    • ants源码中可以看出,自旋锁通过CAS自旋实现,用于控制小区域的、短时间使用的临界区。
    • ants在定义自旋锁时引入了指数退避算法,丰富了自旋锁,这就稍微有点偏超纲了。接下来让我们聚焦于自旋锁的实现。
    2、实现自旋锁的原理

    (1)在ants中通过atomic.CompareAndSwapXXX方法实现CAS自旋

    (2)atomic.CompareAndSwapXXX方法(知识点补充,掌握的读者可以跳过)

    • atomic.CompareAndSwapXXXatomic原子操作包下的一个函数,接收3个参数,1个是目标操作数的地址,第2个是旧值,第3个是新值。返回值是一个布尔类型。

    • 顾名思义,比较并交换。比较目标操作数与旧值,当目标操作数的值等于旧值时,就会被赋予新值,并且返回true;否则返回false

    • 该方法确保了各goroutine互斥地操作目标操作数的同时,保证了目标操作数在两种状态间正确地切换。

    • 通过下面两段代码就知道这个函数的作用了。

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    🚦 三、对比测试

    下面通过多个G竞争自旋锁和互斥锁,对比程序的性能。

    1、场景
    • N个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()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    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()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    (1)可以看到,在本地笔记本的规格上,对于小区域、短时间占用的临界区,使用自旋锁的效率大约是是使用互斥锁的1.6倍。

    • 注意,在不同架构或者不同CPU主频的机器上测试结果可能存在差异,CPU主频越高的机器上,自旋锁与互斥锁的差异越小(我的笔记本为 1.60 GHZ,算是低的了,因此差异较为明显)。
    ⏰四、小结
    1、自旋锁的优点

    (1)自旋锁主要是为了降低了用户进程和内核切换的消耗,适用于等待获得锁时间较短的场景。

    2、自旋锁的缺点

    (1)当锁轮转时间较长,单个线程占用锁的时间较长时,自旋锁循环等待的时间较长,会消耗大量的CPU资源。

    (2)自旋锁无法确保公平性,具体的解决方法有TicketLock

    (3)是否决定使用自旋锁需要综合考虑实际的场景和生产环境。

  • 相关阅读:
    Fisher信息与最大似然估计的渐进正态性(附有在Bernoulli分布上的计算)
    Faster RCNN
    【数据结构初阶】一. 复杂度讲解
    Linux学习:基本命令
    彻底解决vue接入高德地图不显示公司名字问题
    设计模式20——职责链模式
    机器学习之数据准备
    使用Python进行页面开发——Django常用Web工具
    国内首个L3级自动驾驶之城诞生,高精定位和高精地图成为关键支撑
    python的reshape和matlab的reshape,我们不一样
  • 原文地址:https://blog.csdn.net/js010111/article/details/126568547