go-keylock:一种细粒度的流程加锁方式

578 阅读3分钟

一、背景介绍

在并发编程过程中,为了保证某个数据结构或者某个处理流程在同一时刻只能被某一用户或者线程操作,通常会采取加锁的策略。但是在某些特定的场景下,直接使用sync.Mutex加锁的代价是巨大的。例如:我们有一个并发创建某个复杂实例的需求,在这个复杂对象创建过程中需要查找占用大量其他资源,同时不能创建同名的两个实例。由于实例在创建过程中需要修改某不同来源的数据,所以无法通过为数据来源加锁来保证数据的安全的一致。最终我们只能选择在创建实例时加一把大锁。

但事实上,我们并不需要保证同一时刻只能有一个线程去创建一个实例。因为不同名称的实例之间由于涉及的数据来源不同是可以支持并发的。基于上述情况,为了实现一种更细粒度的锁结构,开发了一个keylock工具包,keylock锁可以实现key粒度的锁。针对上面的问题可以实现不同名称的实例创建可以并行,同名实例创建将被阻塞的效果,相比于全局的大锁,提高了系统的并发性能。

keylock工具包的特性:

  • 1、更细的粒度、更高的并发性能
  • 2、使用简单
  • 3、对于常驻程序来说,无需手动删除keyLock对象。
  • 4、基于引用计数的方式回收锁资源。
  • 5、被动式清理,无需额外的定时器。
  • 6、通过sync.pool实现子锁对象的复用,减少内存分配

二、设计和实现

keylock的本质是每个键值都拥有独立的sync.Mutex,从而实现更细粒度的锁机制。因此keyLock的数据结构如下所示:

type LockObj struct {
    Lock *sync.Mutex
    Num  int64
}

type KeyLock struct {
    globalLock sync.Mutex
    locks     map[interface{}]*LockObj
}

由于map结构本身并不是线程安全的,因此增加globalLock来保证locks结构的线程安全。

LockOb中的Num字段是一个取锁计数器,用于记录当前获取该key值对应锁的线程个数,便于后期回收keyLock空间,避免在常驻线程使用keylock对象时,大量创建locks对象但是无法回收空间导致内存泄漏。

由于每个key值都拥有自己的sync.Mutex对象,当并发量很大时,将会频繁的创建sync.Mutex{}对象分配内存空间,为GC带来一些负担。为了缓解对象的创建,基于sync.pool对代码进行了优化。具体如下:

//共享sync.Mutex实例,节约内存空间
var mutexPool = sync.Pool{
    New: func() interface{} {
        return &sync.Mutex{}
    },
}

func (l *KeyLock) getLock(key interface{}) *sync.Mutex {

    l.globalLock.Lock()
    defer l.globalLock.Unlock()

    if lockObj, ok := l.locks[key]; ok {
        atomic.AddInt64(&lockObj.Num, 1)
        return lockObj.Lock
    }
    lock := mutexPool.Get().(*sync.Mutex)
    l.locks[key] = &LockObj{
        Lock: lock,
        Num:  1,
    }
    return lock
}

func (l *KeyLock) Unlock(key interface{}) {
    l.globalLock.Lock()
    defer l.globalLock.Unlock()

    l.locks[key].Lock.Unlock()
    atomic.AddInt64(&l.locks[key].Num, -1)
    //clean
    for _, v := range l.locks {
        if v.Num <= 0 {
            mutexPool.Put(l.locks[key].Lock)
            delete(l.locks, key)
        }
    }
}

完整代码如下所示,也可以直接在github下载:keylock

var mutexPool = sync.Pool{
    New: func() interface{} {
        return &sync.Mutex{}
    },
}

type LockObj struct {
    Lock *sync.Mutex
    Num  int64
}

type KeyLock struct {
    globalLock sync.Mutex
    locks     map[interface{}]*LockObj
}

func NewKeyLock() *KeyLock {
    return &KeyLock{
        locks: make(map[interface{}]*LockObj),
    }
}

func (l *KeyLock) getLock(key interface{}) *sync.Mutex {

    l.globalLock.Lock()
    defer l.globalLock.Unlock()

    if lockObj, ok := l.locks[key]; ok {
        atomic.AddInt64(&lockObj.Num, 1)
        return lockObj.Lock
    }
    lock := mutexPool.Get().(*sync.Mutex)
    l.locks[key] = &LockObj{
        Lock: lock,
        Num:  1,
    }
    return lock
}

func (l *KeyLock) Lock(key interface{}) {
    l.getLock(key).Lock()
}

func (l *KeyLock) Unlock(key interface{}) {
    l.globalLock.Lock()
    defer l.globalLock.Unlock()

    l.locks[key].Lock.Unlock()
    atomic.AddInt64(&l.locks[key].Num, -1)
    //clean
    for _, v := range l.locks {
        if v.Num <= 0 {
            mutexPool.Put(l.locks[key].Lock)
            delete(l.locks, key)
        }
    }
}

三、使用方法

keylock工具包的使用非常简单,只需要下载该包并在代码中导入就可以直接使用。下面是一个使用范例

go get "github.com/sjy3/go-keylock"

import lock "github.com/sjy3/go-keylock"

var keyLock *lock.KeyLock
keyLock = lock.NewKeyLock()

keylock.Lock("shi")
defer keyLock.Unlock("shi")

//需要保证线程安全的操作

四、性能测试

我们对keylock工具包进行了并发和基准测试,使用的电脑平台硬件配置如下:

型号名称: MacBook Pro
型号标识符: MacBookPro16,1
处理器名称: 6-Core Intel Core i7
处理器速度: 2.6 GHz
处理器数目: 1
核总数: 6
L2缓存(每个核): 256 KB
L3缓存: 12 MB
超线程技术: 已启用
内存: 16 GB

测试结果如下:

goos: darwin
goarch: amd64
pkg: go-keylock
BenchmarkCombinationParallel
BenchmarkCombinationParallel-12 2004168 578 ns/op
PASS