golang高性能-无锁编程

文章目录

概要

锁是一种同步机制,用来解决多个线程同时访问临界资源的问题。但是由于在高并发的场景下,频繁的加锁和释放锁,会增加切换上下文的开销,从而降低程序的吞吐量。

无锁编程是一种并发编程技术,主要用于消除多线程编程中锁操作带来的性能损耗。

本文将探讨在特定使用场景下无锁编程和有锁编程的性能差异。选定的场景是栈的操作(出栈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倍

小结

在一些高并发的场景中,某些临界资源需要被频繁地访问,这种时候加锁、释放锁带来的上下文切换开销超过数据操作本省,就可以尝试使用无锁编程。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值