Golang自旋锁的相关介绍

目录
  • 自旋锁
    • golang实现自旋锁
    • 可重入的自旋锁和不可重入的自旋锁
  • 自旋锁的其他变种
    • 1. TicketLock
    • 2. CLHLock
    • 3. MCSLock
    • 4. CLHLock 和 MCSLock
  • 自旋锁与互斥锁
  • 总结

自旋锁

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成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多处理器缓存同步的问题。

自旋锁与互斥锁

  • 自旋锁与互斥锁都是为了实现保护资源共享的机制。
  • 无论是自旋锁还是互斥锁,在任意时刻,都最多只能有一个保持者。
  • 获取互斥锁的线程,如果锁已经被占用,则该线程将进入睡眠状态;获取自旋锁的线程则不会睡眠,而是一直循环等待锁释放。

总结

  • 自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。
  • 自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。
  • 自旋锁如果持有锁的时间太长,则会导致其它等待获取锁的线程耗尽CPU。
  • 自旋锁本身无法保证公平性,同时也无法保证可重入性。
  • 基于自旋锁,可以实现具备公平性和可重入性质的锁。
  • TicketLock:采用类似银行排号叫好的方式实现自旋锁的公平性,但是由于不停的读取serviceNum,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。
  • CLHLock和MCSLock通过链表的方式避免了减少了处理器缓存同步,极大的提高了性能,区别在于CLHLock是通过轮询其前驱节点的状态,而MCS则是查看当前节点的锁状态。
  • CLHLock在NUMA架构下使用会存在问题。在没有cache的NUMA系统架构中,由于CLHLock是在当前节点的前一个节点上自旋,NUMA架构中处理器访问本地内存的速度高于通过网络访问其他节点的内存,所以CLHLock在NUMA架构上不是最优的自旋锁。

到此这篇关于Golang自旋锁的相关介绍的文章就介绍到这了,更多相关Golang自旋锁内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • golang 自旋锁的实现

    CAS算法(compare and swap) CAS算法是一种有名的无锁算法.无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization).CAS算法涉及到三个操作数 需要读写的内存值V 进行比较的值A 拟写入的新值B 当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作).一般情况下是一个自旋操作,即不断的重试.

  • Golang自旋锁的相关介绍

    目录 自旋锁 golang实现自旋锁 可重入的自旋锁和不可重入的自旋锁 自旋锁的其他变种 1. TicketLock 2. CLHLock 3. MCSLock 4. CLHLock 和 MCSLock 自旋锁与互斥锁 总结 自旋锁 获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting. 它是为实现保护共享资源而提出的一种锁机制.其实,自旋锁与互斥锁比较类似,它们都是为了解决某项资源的互斥使用.无论是互斥锁,还是自旋锁,在任何时刻,最多只能由一个保

  • Golang分布式锁详细介绍

    目录 进程内加锁 trylock 基于redis的setnx 基于zk 基于etcd redlock 如何选择 在单机程序并发或并行修改全局变量时,需要对修改行为加锁以创造临界区.为什么需要加锁呢?可以看看下段代码: package main import ( "sync" ) // 全局变量 var counter int func main() { var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go f

  • Java 自旋锁(spinlock)相关知识总结

    一.前言 谈到『自旋锁』,可能大家会说,这有啥好讲的,不就是等待资源的线程"原地打转"嘛.嗯,字面理解的意思很到位,但能深入具体点吗?自旋锁的设计真就这么简单? 本文或者说本系列的目的,都是让大家不要停留在表面,而是深入分析,做到: 灵活使用 掌握原理 优缺点 二.锁的优化:自旋锁 当多个线程想同时访问同一个资源时,就存在资源冲突,这时,大家最直接想到的就是加锁来互斥访问,加锁会有这么几个问题: 等待资源的线程进入睡眠,发生用户态向内核态的切换,有一定的性能开销: 占用资源的线程很快就

  • 简单介绍SQL Server中的自旋锁

    为什么我们需要自旋锁? 用闩锁同步多个线程间数据结构访问,在每个共享数据结构前都放置一个闩锁没有意义的.闩锁与此紧密关联:当你不能获得闩锁(因为其他人已经有一个不兼容的闩锁拿到),查询就会强制等待,并进入挂起(SUSPENDED)状态.查询在挂起状态等待直到可以拿到闩锁,然后就会进入可执行(RUNNABLE)状态.对于查询执行只要没有可用的CPU,查询就一直在可执行(RUNNABLE)状态.一旦CPU有空闲,查询会进入运行(RUNNING)状态,最后成功获取到闩锁,用它来保护访问的共享数据结构.

  • Golang分布式锁简单案例实现流程

    其实锁这种东西,都能能不加就不加,锁会导致程序一定程度上退回到串行化,进而降低效率. 首先,看一个案例,如果要实现一个计数器,并且是多个协程共同进行的,就会出现以下的情况: package main import ( "fmt" "sync" ) func main() { numberFlag := 0 wg := new(sync.WaitGroup) for i := 0; i < 200; i++ { wg.Add(1) go func() { def

  • Java线程并发中常见的锁机制详细介绍

    随着互联网的蓬勃发展,越来越多的互联网企业面临着用户量膨胀而带来的并发安全问题.本文着重介绍了在java并发中常见的几种锁机制. 1.偏向锁 偏向锁是JDK1.6提出来的一种锁优化的机制.其核心的思想是,如果程序没有竞争,则取消之前已经取得锁的线程同步操作.也就是说,若某一锁被线程获取后,便进入偏向模式,当线程再次请求这个锁时,就无需再进行相关的同步操作了,从而节约了操作时间,如果在此之间有其他的线程进行了锁请求,则锁退出偏向模式.在JVM中使用-XX:+UseBiasedLocking pac

  • 什么是Java自旋锁

    目录 1.自旋锁 2.工作流程 3.缺点 4.实现原理 5.自适应自旋 前言: 阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间.如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长. 1.自旋锁 在有些场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失. 如果机器有多个CPU核心,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间

  • Golang的锁机制与使用技巧小结

    目录 1. sync.Mutex详解 2. RWMutex详解 3. sync.Map详解 4. 原子操作 atomic.Value 5. 使用小技巧 1. sync.Mutex详解 sync.Mutex是Go中的互斥锁,通过.lock()方法上锁,.unlock()方法解锁.需要注意的是,因为Go函数值传递的特点,sync.Mutex通过函数传递时,会进行一次拷贝,所以传递过去的锁是一把全新的锁,大家在使用时要注意这一点,另外sync.Mutex是非重入锁,这一点要与Java中的锁区分. ty

  • Java实现手写自旋锁的示例代码

    目录 前言 自旋锁 原子性 自己动手写自旋锁 自己动手写可重入自旋锁 总结 前言 我们在写并发程序的时候,一个非常常见的需求就是保证在某一个时刻只有一个线程执行某段代码,像这种代码叫做临界区,而通常保证一个时刻只有一个线程执行临界区的代码的方法就是锁.在本篇文章当中我们将会仔细分析和学习自旋锁,所谓自旋锁就是通过while循环实现的,让拿到锁的线程进入临界区执行代码,让没有拿到锁的线程一直进行while死循环,这其实就是线程自己“旋”在while循环了,因而这种锁就叫做自旋锁. 自旋锁 原子性

随机推荐