1.自旋锁
假设我们有一个变量初始值是0,有两个线程会持续不断的将n的数值增一,为避免出现并发问题,我们需要一把锁来保护变量n.
锁,本质上也是一个变量,我们可以将锁的初始值置为0,
若能够原子性的将锁的值由0置为1,就可以获得锁,然后操作变量n,
操作结束后,再原子性的将锁的值由1置为0,就可以释放锁.
但现在有两个线程都会尝试获得锁,而同一时刻只有一个能够成功,那另一个没有获得锁的线程该怎么办呢?直观的想法就是如果不成功,就一直尝试,直到能够获得锁为止,这就是"自旋锁"
2.Go语言实现一把自旋锁
先来构造应用场景,有两个协程,代号分别为0和1,它们都在循环中先获得锁,一个把变量n自增1,一个把变量自减1,然后释放锁,两个协程各自循环一亿次,最后打印变量n的值
再来看这个锁的实现,我们使用Go语言atomic提供的CompareAndSwapInt32函数实现锁变量的原子操作,并在获取锁失败时不断重复尝试
实际运行看看CPU的使用情况,在双核CPU的虚拟机上,使用率将近百分置二百,也就是说跑满了CPU,这是因为在双核环境下runtime会默认分配两个P,所以上述两个协程会在两个线程上并行执行,当一个协程持有锁的时候,另一个协程会持续高频的重复尝试获得锁,空耗CPU的情况就会比较严重,所以在工程应用中,通常不会只用简单的循环来实现自旋锁,
例如在nigin的自旋锁实现中,会在获取锁失败的情况下调用ngx_cpu_pause函数,而这个函数实际上会调用汇编上的pause指令,pause指令的主要作用一方面是防止内存乱序,另一方面是减少CPU的功耗,下面,我们也这样优化下看看效果,
我们找到Go语言中CompareAndSwapInt32函数的底层实现,真正实现在runtime/internal/atomic包里的Cas函数中,我们把这个实现拿出来改动一下,CMPXCHGL这条指令就是个CAS操作,我们在它后面用条件跳转指令构造一个循环,在CAS操作不成功时先执行PAUSE指令,然后循环尝试,直至CAS成功,需要注意的是,CMPXCHGL在替代不成功时,会把给定地址处的旧值写入AX寄存器中,所以我们需要在每次循环开始时重新为AX赋值,这种完全基于汇编实现还是有些复杂,实际上用汇编实现的只有PAUSE这条指令而已,所以我们不如单独实现一个汇编班的pause函数,由参数指定循环执行PAUSE指令的次数,然后使用这个pause函数结合标准库的atomic包,再写一个优化版的自旋锁,接下来指定PAUSE指令循环次数为30次
在此运行这个函数,让我们看看CPU的使用情况,同样双核环境,CPU使用率居然没有什么变化,那么增加PAUSE指令还有什么用呢,再来看两次程序运行时间,增加PAUSE指令之前程序运行了16秒,增加PAUSE指令之后程序只运行6秒,