Good way to return on locked mutex in go

huangapple go评论90阅读模式
英文:

Good way to return on locked mutex in 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
    }()

    // 做一些事情

}

我不喜欢这个方法的原因:

  • 看起来很混乱
  • 很容易错误地在通道上阻塞/错误地向通道写入数据

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()

    // 做一些事情

    return nil
}

我也不喜欢这个方法。使用带有互斥锁的共享变量不太好。

还有其他的想法吗?

英文:

Following problem:
I have a function that only should allow one caller to execute.
If someone tries to call the function and it is already busy the second caller should immediatly return with an error.

I tried the following:

1. Use a mutex

Would be pretty easy. But the problem is, you cannot check if a mutex is locked. You can only block on it. Therefore it does not work

2. Wait on a channel

var canExec = make(chan bool, 1)

func init() {
	canExec &lt;- true
}

func onlyOne() error {
	select {
	case &lt;-canExec:
	default:
		return errors.New(&quot;already busy&quot;)
	}

	defer func() {
		fmt.Println(&quot;done&quot;)
		canExec &lt;- true
	}()

	// do stuff

}

What I don't like here:

  • looks really messi
  • if easy to mistakenly block on the channel / mistakenly write to the channel

3. Mixture of mutex and shared state

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(&quot;busy&quot;)
	}
	defer endExec()

	// do stuff

	return nil
}

I don't like this either. Using a shard variable with mutex is not that nice.

Any other idea?

答案1

得分: 36

我会把你提供的内容翻译成中文,以下是翻译的结果:

我会提出我的偏好 - 使用atomic包

var (
    locker    uint32
    errLocked = errors.New("被锁住了伙计")
)

func OneAtATime(d time.Duration) error {
    if !atomic.CompareAndSwapUint32(&locker, 0, 1) { // <-----------------------------
        return errLocked                             //   所有逻辑都在这四行里面         |
    }                                                //   这四行代码                   |
    defer atomic.StoreUint32(&locker, 0)             // <-----------------------------

    // 在这里进行逻辑处理,但是我们会休眠
    time.Sleep(d)

    return nil
}

这个想法非常简单。将初始值设置为0(uint32的零值)。函数中的第一件事是检查locker的值是否为0,如果是,则将其更改为1。所有这些操作都是原子性的。如果失败,简单地返回一个错误(或者以其他方式处理被锁定的状态)。如果成功,立即延迟将值(现在为1)替换为0。当然你不一定非要使用defer,但是如果在返回之前不将值设置回0,那么函数将无法再次运行。

在完成这4行设置之后,你可以按照正常的方式进行其他操作。

你可以通过为状态使用命名值来使代码更加清晰。

const (
    stateUnlocked uint32 = iota
    stateLocked
)

var (
    locker    = stateUnlocked
    errLocked = errors.New("被锁住了伙计")
)

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

    // 在这里进行逻辑处理,但是我们会休眠
    time.Sleep(d)

    return nil
}
英文:

I'll throw my preference out there - use the atomic package.

var (
	locker    uint32
	errLocked = errors.New(&quot;Locked out buddy&quot;)
)

func OneAtATime(d time.Duration) error {
	if !atomic.CompareAndSwapUint32(&amp;locker, 0, 1) { // &lt;-----------------------------
		return errLocked                             //   All logic in these         |
	}                                                //   four lines                 |
	defer atomic.StoreUint32(&amp;locker, 0)             // &lt;-----------------------------
	
    // logic here, but we will sleep
	time.Sleep(d)

	return nil
}

The idea is pretty simple. Set the initial value to 0 (0 value of uint32). The first thing you do in the function is check if the value of locker is currently 0 and if so it changes it to 1. It does all of this atomically. If it fails simply return an error (or however else you like to handle a locked state). If successful, you immediately defer replacing the value (now 1) with 0. You don't have to use defer obviously, but failing to set the value back to 0 before returning would leave you in a state where the function could no longer be run.

After you do those 4 lines of setup, you do whatever you would normally.

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

You can make things a little nicer if desired by using named values for your states.

const (
	stateUnlocked uint32 = iota
	stateLocked
)

var (
	locker    = stateUnlocked
	errLocked = errors.New(&quot;Locked out buddy&quot;)
)

func OneAtATime(d time.Duration) error {
	if !atomic.CompareAndSwapUint32(&amp;locker, stateUnlocked, stateLocked) {
		return errLocked
	}
	defer atomic.StoreUint32(&amp;locker, stateUnlocked)
	
    // logic here, but we will sleep
	time.Sleep(d)

	return nil
}

答案2

得分: 31

你可以使用信号量来实现这个(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
}

希望对你有帮助!

英文:

You can use a semaphore for this (go get golang.org/x/sync/semaphore)

package main

import (
	&quot;errors&quot;
	&quot;fmt&quot;
	&quot;sync&quot;
	&quot;time&quot;

	&quot;golang.org/x/sync/semaphore&quot;
)

var sem = semaphore.NewWeighted(1)

func main() {
	var wg sync.WaitGroup
	for i := 0; i &lt; 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(&quot;busy&quot;)
	}
	defer sem.Release(1)
	fmt.Println(&quot;working&quot;)
	time.Sleep(5 * time.Second)
	return nil
}

答案3

得分: 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("错误:", err)
            } else {
                fmt.Println("好的")
            }
        }()
        go func() {
            ch <- true
        }()
    }

    wg.Wait()
}


func onlyOne() error {
    select {
    case <-ch:
        // 做一些事情
        return nil
    default:
        return errors.New("忙碌")
    }
}

请注意,我只翻译了代码部分,其他内容被忽略了。

英文:

You could use standard channel approach with select statement.

var (
	ch = make(chan bool)
)

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

	wg.Wait()
}


func onlyOne() error {
	select {
	case &lt;-ch:
		// do stuff
		return nil
	default:
		return errors.New(&quot;Busy&quot;)
	}
}

答案4

得分: 4

你想要一个函数只执行一次,还是在给定的时间内执行一次?如果是前者,请参考https://golang.org/pkg/sync/#Once。

如果你想要一次执行一个解决方案:

package main

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

// OnceAtATime 保护函数不被同时执行。
// 示例:
//    func myFunc() { time.Sleep(10*time.Second) }
//    func main() {
//        once := OnceAtATime{}
//        once.Do(myFunc)
//        once.Do(myFunc) // 不会执行
//    }
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

英文:

Do you want a function to be executed exactly once or once at given time? In former case take a look at https://golang.org/pkg/sync/#Once.

If you want once at a time solution:

package main

import (
	&quot;fmt&quot;
	&quot;sync&quot;
	&quot;time&quot;
)

// 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&lt;- struct{}) {
	for i := 0; i &lt; 10; i++ {
		fmt.Printf(&quot;%d: %d\n&quot;, 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) })
	&lt;-done
	done = make(chan struct{})
	go once.Do(func() { f(3, done) })
	&lt;-done

}

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

答案5

得分: 2

但问题是,你无法检查互斥锁是否被锁定。你只能在其上阻塞。因此,它不起作用。

可能在 Go 1.18(2022年第一季度)中,你将能够测试互斥锁是否被锁定...而不会阻塞。

参考 Go 101 提到的 issue 45435,由 Tye McQueen 提出:

sync: 添加 Mutex.TryLock

随后是 CL 319769,但有一个警告:

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

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

最初的反对意见(已经撤回)是:

锁用于保护不变量。
如果锁被其他人持有,你对不变量无法做出任何断言。

TryLock 鼓励对锁的不精确思考;它鼓励对可能为真或可能为假的不变量做出假设。这最终成为竞争的源头。

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

还有:

基于通道的实现是可能的,但性能比较差。
我们之所以有 sync.Mutex 而不仅仅使用通道进行锁定,是有原因的。

英文:

> But the problem is, you cannot check if a mutex is locked. You can only block on it. Therefore it does not work

With possible Go 1.18 (Q1 2022), you will be able to test if a mutex is locked... without blocking on it.

See (as mentioned by Go 101) the issue 45435 from Tye McQueen :

> ## sync: add Mutex.TryLock

This is followed by CL 319769, with the caveat:

> Use of these functions is almost (but not) always a bad idea.
>
> Very rarely they are necessary, and third-party implementations (using a mutex and an atomic word, say) cannot integrate as well with the race detector as implementations in package sync itself.

The objections (since retracted) were:

> Locks are for protecting invariants.
If the lock is held by someone else, there is nothing you can say about the invariant.
>
> TryLock encourages imprecise thinking about locks; it encourages making assumptions about the invariants that may or may not be true.
That ends up being its own source of races.
>
> Thinking more about this, there is one important benefit to building TryLock into Mutex, compared to a wrapper:
failed TryLock calls wouldn't create spurious happens-before edges to confuse the race detector.

And:

> A channel-based implementation is possible, but performs poorly in comparison.
There's a reason we have sync.Mutex rather than just using channel for locking.

答案6

得分: 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-

英文:

I came up with the following generic solution for that:

Works for me, or do you see any problem with that?

import (
 &quot;sync&quot;
)

const (
	ONLYONECALLER_LOCK = &quot;onlyonecaller&quot;
	ANOTHER_LOCK = &quot;onlyonecaller&quot;
)

var locks = map[string]bool{}

var mutex = &amp;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
}

see: https://play.golang.org/p/vUUsHcT3L-

答案7

得分: 0

这个包怎么样:https://github.com/viney-shih/go-lock。它使用通道信号量golang.org/x/sync/semaphore)来解决你的问题。

go-lock除了提供Lock和Unlock函数外,还实现了TryLock、TryLockWithTimeout和TryLockWithContext函数。它提供了灵活性来控制资源。

示例代码:

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()) // 返回 false

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

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

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


    // 输出:
    // Return false
    // Return false
    // Return false
}
英文:

How about this package: https://github.com/viney-shih/go-lock . It use channel and semaphore (golang.org/x/sync/semaphore) to solve your problem.

go-lock implements TryLock, TryLockWithTimeout and TryLockWithContext functions in addition to Lock and Unlock. It provides flexibility to control the resources.

Examples:

package main

import (
    &quot;fmt&quot;
    &quot;time&quot;
    &quot;context&quot;

    lock &quot;github.com/viney-shih/go-lock&quot;
)

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

    casMut.Lock()
    defer casMut.Unlock()

    // TryLock without blocking
    fmt.Println(&quot;Return&quot;, casMut.TryLock()) // Return false

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

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

    fmt.Println(&quot;Return&quot;, casMut.TryLockWithContext(ctx)) // Return false
    

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

答案8

得分: 0

让我们保持简单:

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

    var sem *semaphore.NewWeighted(1)

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

    func doSomething() {
      if !sem.TryAcquire(1) {
        return errors.New("我很忙")
      }
      defer sem.Release(1)
      fmt.Println("我正在做我的工作,然后我会小睡一会儿")
      time.Sleep(10)
    }

    func main() {
      go func() {
        doSomething()
      }()
    }
英文:

Lets keep it simple:

    package main
    
    import (
     &quot;fmt&quot;
     &quot;time&quot;
     &quot;golang.org/x/sync/semaphore&quot;
    )

    var sem *semaphore.NewWeighted(1)

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

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

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

huangapple
  • 本文由 发表于 2017年7月20日 15:46:56
  • 转载请务必保留本文链接:https://go.coder-hub.com/45208536.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定