概要
锁是一种同步机制,用来解决多个线程同时访问临界资源的问题。但是由于在高并发的场景下,频繁的加锁和释放锁,会增加切换上下文的开销,从而降低程序的吞吐量。
无锁编程是一种并发编程技术,主要用于消除多线程编程中锁操作带来的性能损耗。
本文将探讨在特定使用场景下无锁编程和有锁编程的性能差异。选定的场景是栈的操作(出栈Pop操作、入栈Push操作),通过并发编程的形式,多个线程同时对栈进行操作。
代码
stack接口
- Push函数:将一个元素进栈
- Pop函数:将一个元素出栈
package lock_freeExamples
type StackInterface interface {
Push(interface{})
Pop() interface{}
}
有锁编程
由于可能有多个线程同时对栈进行操作,在每次对栈进行操作之前,都需要进行加锁,操作完成之后再对锁进行释放
package lock_freeExamples
import "sync"
// 互斥锁实现的栈(有锁编程)
type MutexStack struct {
// 栈元素容器用切片表示
v []interface{}
// 互斥锁
mu sync.Mutex
}
/**
* NewMutexStack
* @Description: return mutexstack
* @return *MutexStack
*/
func NewMutexStack() *MutexStack {
return &MutexStack{v: make([]interface{}, 0)}
}
/**
* Push
* @Description: push element into stack
* @receiver s
* @param v
*/
func (s *MutexStack) Push(v interface{}) {
// 可能同时有多个 goroutine 操作
// stack 属于临界区资源,需要加锁
s.mu.Lock()
s.v = append(s.v, v)
s.mu.Unlock()
}
/**
* Pop
* @Description: pop element out of stack
* @receiver s
* @return interface{}
*/
func (s *MutexStack) Pop() interface{} {
// 可能同时有多个 goroutine 操作
// stack 属于临界区资源,需要加锁
s.mu.Lock()
var v interface{}
if len(s.v) > 0 {
v = s.v[len(s.v)-1]
s.v = s.v[:len(s.v)-1]
}
s.mu.Unlock()
return v
}
无锁编程
无锁编程中使用了很多原子操作来保证并发的准确性
- atomic.LoadPointer:取操作的原子操作
- atomic.AddUint64:加操作的原子操作
- atomic.CompareAndSwapPointer:CAS的原子操作。比较和交换操作,先将新值与旧值进行比较,如果值一致说明数据没有被修改,则进行交换操作。如果值不同,说明数据已被修改,则不进行相应操作
package lock_freeExamples
import (
"sync/atomic"
"unsafe"
)
// 栈的节点
type directItem struct {
next unsafe.Pointer
v interface{}
}
// 无锁栈
type LockFreeStack struct {
top unsafe.Pointer
len uint64
}
/**
* NewLockFreeStack
* @Description: return a new LockFreeStack
* @return *LockFreeStack
*/
func NewLockFreeStack() *LockFreeStack {
return &LockFreeStack{}
}
/**
* Push
* @Description: push an element into stack
* @receiver l
* @param v
*/
func (l *LockFreeStack) Push(v interface{}) {
element := directItem{v: v}
var top unsafe.Pointer
for {
// 原子载入操作(可以避免读到一半进行了修改)
top = atomic.LoadPointer(&l.top)
element.next = top
if atomic.CompareAndSwapPointer(&l.top, top, unsafe.Pointer(&element)) {
// 只有一个 goroutine 可以进到这里
atomic.AddUint64(&l.len, 1)
return
}
}
}
/**
* Pop
* @Description: pop an element out of stack
* @receiver l
* @return interface{}
*/
func (l *LockFreeStack) Pop() interface{} {
var top, next unsafe.Pointer
var element *directItem
for {
top = atomic.LoadPointer(&l.top)
if top == nil {
return nil
}
// 类型转换,将 unsafe.pointer 转换成 *directItem
element = (*directItem)(top)
next = atomic.LoadPointer(&element.next)
if atomic.CompareAndSwapPointer(&l.top, top, next) {
atomic.AddUint64(&l.len, ^uint64(0))
return element.v
}
}
}
性能测试
package lock_freeExamples
import (
"fmt"
"math/rand"
"sync/atomic"
"testing"
"time"
)
func Benchmark_Stack(b *testing.B) {
rand.Seed(time.Now().UnixNano())
length := 1 << 12
inputs := make([]int, length)
// 设置测试参数
for key, _ := range inputs {
inputs[key] = rand.Int()
}
ms, ls := NewMutexStack(), NewLockFreeStack()
b.ResetTimer()
for _, value := range [...]StackInterface{ls, ms} {
b.Run(fmt.Sprintf("%T", value), func(b *testing.B) {
// 初始数值0
var c int64
// 创建多个 goroutine 进行测试
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
i := int(atomic.AddInt64(&c, 1)-1) % length
v := inputs[i]
if v >= 0 {
value.Push(v)
} else {
value.Pop()
}
}
})
})
}
}
性能测试结果
结论:无锁编程的性能约为有锁编程的1.8倍
小结
在一些高并发的场景中,某些临界资源需要被频繁地访问,这种时候加锁、释放锁带来的上下文切换开销超过数据操作本省,就可以尝试使用无锁编程。