在某些场景,我们只希望一个任务有单一的执行者,而不像计数器一样,所有的 Goroutine 都成功执行。后续的 Goroutine 在抢锁失败后,需要放弃执行,这时候就需要尝试加锁 / trylock

尝试加锁,在加锁成功后执行后续流程,失败时不可以阻塞,而是直接返回加锁的结果。

在 Go 语言中可以用大小为 1 的 Channel 来模拟 trylock:

// Lock try lock
type Lock struct {
    c chan struct{}
}

// Lock try lock, return lock result
func (l Lock) Lock() bool {
    result := false
    select {
    case <-l.c:
        result = true
    default:
    }
    return result
}

// Unlock the try lock
func (l Lock) Unlock() {
    l.c <- struct{}{}
}

// NewLock generate a try lock
func NewLock() Lock {
    var l Lock
    l.c = make(chan struct{}, 1)
    l.c <- struct{}{}
    return l
}

func main() {
    var lock = NewLock()
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            if !lock.Lock() {
                println("lock failed")
                return
            }
            counter++
            println("current counter: ", counter)
            lock.Unlock()
        }()
    }
    wg.Wait()
}

每个 Goruntine 只有成功执行了Lock()才会继续执行后续代码,因此在Unlock()时可以保证Lock结构体里的 Channel 一定是空的,所以不会阻塞也不会失败。

在单机系统中,trylock 并不是一个好选择,因为大量的 Goruntine 抢锁会无意义地占用 cpu 资源,这就是活锁。

活锁指是的程序看起来在正常执行,但 cpu 周期被浪费在抢锁而非执行任务上,从而程序整体的执行效率低下。活锁的问题定位起来很麻烦,所以在单机场景下,不建议使用这种锁。

最后编辑: kuteng  文档更新时间: 2022-03-22 19:29   作者:kuteng