在go中锁定互斥锁的好方法

时间:2017-07-20 07:46:57

标签: go

以下问题: 我有一个只允许一个调用者执行的函数。 如果有人试图调用该函数并且它已经忙,则第二个调用者应该立即返回错误。

我尝试了以下内容:

1。使用互斥锁

会很容易的。但问题是,您无法检查互斥锁是否被锁定。你只能阻止它。因此它不起作用

2。等待频道

var canExec = make(chan bool, 1)

func init() {
    canExec <- true
}

func onlyOne() error {
    select {
    case <-canExec:
    default:
        return errors.New("already busy")
    }

    defer func() {
        fmt.Println("done")
        canExec <- true
    }()

    // do stuff

}

我不喜欢这里:

  • 看起来很麻烦
  • 如果容易错误地阻止频道/错误地写入频道

第3。互斥和混合状态的混合

var open = true

var myMutex *sync.Mutex

func canExec() bool {
    myMutex.Lock()
    defer myMutex.Unlock()

    if open {
        open = false
        return true
    }

    return false
}

func endExec() {
    myMutex.Lock()
    defer myMutex.Unlock()

    open = true
}

func onlyOne() error {
    if !canExec() {
        return errors.New("busy")
    }
    defer endExec()

    // do stuff

    return nil
}

我也不喜欢这样。使用带有互斥锁的分片变量并不是那么好。

还有其他想法吗?

8 个答案:

答案 0 :(得分:14)

我会抛弃我的偏好 - 使用atomic package

var (
    locker    uint32
    errLocked = errors.New("Locked out buddy")
)

func OneAtATime(d time.Duration) error {
    if !atomic.CompareAndSwapUint32(&locker, 0, 1) { // <-----------------------------
        return errLocked                             //   All logic in these         |
    }                                                //   four lines                 |
    defer atomic.StoreUint32(&locker, 0)             // <-----------------------------

    // logic here, but we will sleep
    time.Sleep(d)

    return nil
}

这个想法非常简单。将初始值设置为0(uint32的0值)。你在函数中做的第一件事是检查locker的值当前是否为0,如果是,则将其更改为1.它以原子方式执行所有操作。如果失败则只返回错误(或者你喜欢处理锁定状态)。如果成功,您立即将值(现在为1)更改为0.您不必显然使用延迟,但在返回之前未将值设置回0将使您处于函数无法执行的状态更长的时间。

完成这4行设置之后,你可以做任何你想做的事。

https://play.golang.org/p/riryVJM4Qf

如果需要,可以通过为州使用命名值来使事情变得更好。

const (
    stateUnlocked uint32 = iota
    stateLocked
)

var (
    locker    = stateUnlocked
    errLocked = errors.New("Locked out buddy")
)

func OneAtATime(d time.Duration) error {
    if !atomic.CompareAndSwapUint32(&locker, stateUnlocked, stateLocked) {
        return errLocked
    }
    defer atomic.StoreUint32(&locker, stateUnlocked)

    // logic here, but we will sleep
    time.Sleep(d)

    return nil
}

答案 1 :(得分:4)

您可以使用标准通道方法和select语句。

var (
    ch = make(chan bool)
)

func main() {
    i := 0
    wg := sync.WaitGroup{}
    for i < 100 {
        i++
        wg.Add(1)
        go func() {
            defer wg.Done()
            err := onlyOne()
            if err != nil {
                fmt.Println("Error: ", err)
            } else {
                fmt.Println("Ok")
            }
        }()
        go func() {
            ch <- true
        }()
    }

    wg.Wait()
}


func onlyOne() error {
    select {
    case <-ch:
        // do stuff
        return nil
    default:
        return errors.New("Busy")
    }
}

答案 2 :(得分:2)

您可以使用信号量(go get golang.org/x/sync/semaphore

package main

import (
    "errors"
    "fmt"
    "sync"
    "time"

    "golang.org/x/sync/semaphore"
)

var sem = semaphore.NewWeighted(1)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            if err := onlyOne(); err != nil {
                fmt.Println(err)
            }
        }()
        time.Sleep(time.Second)
    }
    wg.Wait()
}

func onlyOne() error {
    if !sem.TryAcquire(1) {
        return errors.New("busy")
    }
    defer sem.Release(1)
    fmt.Println("working")
    time.Sleep(5 * time.Second)
    return nil
}

答案 3 :(得分:1)

您希望函数在给定时间执行一次或一次吗?在前一种情况下,请查看https://golang.org/pkg/sync/#Once

如果你想要一次解决一次:

package main

import (
    "fmt"
    "sync"
    "time"
)

// OnceAtATime protects function from being executed simultaneously.
// Example:
//    func myFunc() { time.Sleep(10*time.Second) }
//    func main() {
//        once := OnceAtATime{}
//        once.Do(myFunc)
//        once.Do(myFunc) // not executed
//    }
type OnceAtATime struct {
    m        sync.Mutex
    executed bool
}

func (o *OnceAtATime) Do(f func()) {
    o.m.Lock()
    if o.executed {
        o.m.Unlock()
        return
    }
    o.executed = true
    o.m.Unlock()
    f()
    o.m.Lock()
    o.executed = false
    o.m.Unlock()
}

// Proof of concept
func f(m int, done chan<- struct{}) {
    for i := 0; i < 10; i++ {
        fmt.Printf("%d: %d\n", m, i)
        time.Sleep(250 * time.Millisecond)
    }
    close(done)
}

func main() {
    done := make(chan struct{})
    once := OnceAtATime{}

    go once.Do(func() { f(1, done) })
    go once.Do(func() { f(2, done) })
    <-done
    done = make(chan struct{})
    go once.Do(func() { f(3, done) })
    <-done

}

https://play.golang.org/p/nZcEcWAgKp

答案 4 :(得分:1)

<块引用>

但问题是,您无法检查互斥锁是否已锁定。你只能阻止它。因此它不起作用

随着可能的 Go 1.18(2022 年第一季度),您将能够测试互斥锁是否被锁定......不会阻塞它。

看到(如mentioned by Go 101)来自issue 45435Tye McQueen

<块引用>

sync:添加Mutex.TryLock

后面跟着 CL 319769,注意:

<块引用>

使用这些函数几乎(但并非)总是一个坏主意。

它们很少是必要的,并且第三方实现(例如使用互斥锁和原子词)无法像包同步本身中的实现那样与竞争检测器集成。

The objections(自撤回后)是:

<块引用>

锁是用来保护不变量的。
如果锁是由其他人持有的,那么关于不变量你就无话可说了。

TryLock 鼓励对锁的不精确思考;它鼓励对可能正确也可能不正确的不变量做出假设。
这最终成为它自己的种族来源。

仔细考虑一下,与包装器相比,将 TryLock 构建到 Mutex 中有一个重要的好处:
失败的 TryLock 调用不会创建虚假的happens-before 边缘来混淆竞争检测器。

还有:

<块引用>

基于通道的实现是可能的,但相比之下表现不佳。
我们使用 sync.Mutex 而不仅仅是使用通道进行锁定是有原因的。

答案 5 :(得分:0)

我想出了以下通用解决方案:

对我有用,或者你觉得有什么问题吗?

import (
 "sync"
)

const (
    ONLYONECALLER_LOCK = "onlyonecaller"
    ANOTHER_LOCK = "onlyonecaller"
)

var locks = map[string]bool{}

var mutex = &sync.Mutex{}

func Lock(lock string) bool {
    mutex.Lock()
    defer mutex.Unlock()

    locked, ok := locks[lock]
    if !ok {
        locks[lock] = true
        return true
    }

    if locked {
        return false
    }

    locks[lock] = true
    return true
}

func IsLocked(lock string) bool {
    mutex.Lock()
    defer mutex.Unlock()

    locked, ok := locks[lock]
    if !ok {
        return false
    }

    return locked
}

func Unlock(lock string) {
    mutex.Lock()
    defer mutex.Unlock()

    locked, ok := locks[lock]
    if !ok {
        return
    }

    if !locked {
        return
    }

    locks[lock] = false
}

请参阅:https://play.golang.org/p/vUUsHcT3L-

答案 6 :(得分:0)

该软件包如何:https://github.com/viney-shih/go-lock。它使用频道信号量golang.org/x/sync/semaphore)解决您的问题。

go-lock除了锁定和解锁之外,还实现了TryLockTryLockWithTimeoutTryLockWithContext功能。它提供了控制资源的灵活性。

示例:

package main

import (
    "fmt"
    "time"
    "context"

    lock "github.com/viney-shih/go-lock"
)

func main() {
    casMut := lock.NewCASMutex()

    casMut.Lock()
    defer casMut.Unlock()

    // TryLock without blocking
    fmt.Println("Return", casMut.TryLock()) // Return false

    // TryLockWithTimeout without blocking
    fmt.Println("Return", casMut.TryLockWithTimeout(50*time.Millisecond)) // Return false

    // TryLockWithContext without blocking
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()

    fmt.Println("Return", casMut.TryLockWithContext(ctx)) // Return false


    // Output:
    // Return false
    // Return false
    // Return false
}

答案 7 :(得分:0)

让我们保持简单:

    package main
    
    import (
     "fmt"
     "time"
     "golang.org/x/sync/semaphore"
    )

    var sem *semaphore.NewWeighted(1)

    func init() {
      sem = emaphore.NewWeighted(1)
    }

    func doSomething() {
      if !sem.TryAcquire(1) {
        return errors.New("I'm busy")
      }
      defer sem.Release(1)
      fmt.Println("I'm doing my work right now, then I'll take a nap")
      time.Sleep(10)
    }

    func main() {
      go func() {
        doSomething()
      }()
    }