• 企业400电话
  • 微网小程序
  • AI电话机器人
  • 电商代运营
  • 全 部 栏 目

    企业400电话 网络优化推广 AI电话机器人 呼叫中心 网站建设 商标✡知产 微网小程序 电商运营 彩铃•短信 增值拓展业务
    golang 自旋锁的实现

    CAS算法(compare and swap)

    CAS算法是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

    当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

    自旋锁

    自旋锁是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断地判断是否能够被成功获取,知直到获取到锁才会退出循环。

    获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成 busy-waiting 。

    它是为实现保护共享资源而提出的一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能由一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,“自旋”一词就是因此而得名。

    golang实现自旋锁

    type spinLock uint32
    func (sl *spinLock) Lock() {
      for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
        runtime.Gosched()
      }
    }
    func (sl *spinLock) Unlock() {
      atomic.StoreUint32((*uint32)(sl), 0)
    }
    func NewSpinLock() sync.Locker {
      var lock spinLock
      return lock
    }
    

    可重入的自旋锁和不可重入的自旋锁

    文章开始的时候的那段代码,仔细分析一下就可以看出,它是不支持重入的,即当一个线程第一次已经获取到了该锁,在锁释放之前又一次重新获取该锁,第二次就不能成功获取到。由于不满足CAS,所以第二次获取会进入while循环等待,而如果是可重入锁,第二次也是应该能够成功获取到的。

    而且,即使第二次能够成功获取,那么当第一次释放锁的时候,第二次获取到的锁也会被释放,而这是不合理的。

    为了实现可重入锁,我们需要引入一个计数器,用来记录获取锁的线程数

    type spinLock struct {
       owner int
       count int
    }
    
    func (sl *spinLock) Lock() {
        me := GetGoroutineId()
        if spinLock .owner == me { // 如果当前线程已经获取到了锁,线程数增加一,然后返回
            sl.count++
            return
        }
        // 如果没获取到锁,则通过CAS自旋
      for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
        runtime.Gosched()
      }
    }
    func (sl *spinLock) Unlock() {
       if rl.owner != GetGoroutineId() {
         panic("illegalMonitorStateError")
       }
       if sl.count >0 { // 如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟
          sl.count--
        }else { // 如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。
          atomic.StoreUint32((*uint32)(sl), 0)
        }
    }
    
    func GetGoroutineId() int {
      defer func() {
        if err := recover(); err != nil {
          fmt.Println("panic recover:panic info:%v", err)   }
      }()
    
      var buf [64]byte
      n := runtime.Stack(buf[:], false)
      idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]
      id, err := strconv.Atoi(idField)
      if err != nil {
        panic(fmt.Sprintf("cannot get goroutine id: %v", err))
      }
      return id
    }
    
    
    func NewSpinLock() sync.Locker {
      var lock spinLock
      return lock
    }
    
    

    自旋锁的其他变种

    1. TicketLock

    TicketLock主要解决的是公平性的问题。

    思路:每当有线程获取锁的时候,就给该线程分配一个递增的id,我们称之为排队号,同时,锁对应一个服务号,每当有线程释放锁,服务号就会递增,此时如果服务号与某个线程排队号一致,那么该线程就获得锁,由于排队号是递增的,所以就保证了最先请求获取锁的线程可以最先获取到锁,就实现了公平性。

    可以想象成银行办理业务排队,排队的每一个顾客都代表一个需要请求锁的线程,而银行服务窗口表示锁,每当有窗口服务完成就把自己的服务号加一,此时在排队的所有顾客中,只有自己的排队号与服务号一致的才可以得到服务。

    2. CLHLock

    CLH锁是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋,获得锁。

    3. MCSLock

    MCSLock则是对本地变量的节点进行循环。

    4. CLHLock 和 MCSLock

    都是基于链表,不同的是CLHLock是基于隐式链表,没有真正的后续节点属性,MCSLock是显示链表,有一个指向后续节点的属性。

    将获取锁的线程状态借助节点(node)保存,每个线程都有一份独立的节点,这样就解决了TicketLock多处理器缓存同步的问题。

    自旋锁与互斥锁

    总结:

    以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

    您可能感兴趣的文章:
    • 详解golang RWMutex读写互斥锁源码分析
    • 详解Golang互斥锁内部实现
    上一篇:用gomock进行mock测试的方法示例
    下一篇:Go语言中strings和strconv包示例代码详解
  • 相关文章
  • 

    © 2016-2020 巨人网络通讯 版权所有

    《增值电信业务经营许可证》 苏ICP备15040257号-8

    golang 自旋锁的实现 golang,自旋,锁,的,实现,golang,