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 <- true } func onlyOne() error { select { case <-canExec: default: return errors.New("already busy") } defer func() { fmt.Println("done") canExec <- true }() // do stuff }
What I don't like here:
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("busy") } 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?
Mutexes are used to protect shared resources. If the mutex is already locked by another thread, the thread waits for the mutex to become available. The thread that has locked a mutex becomes its current owner and remains the owner until the same thread has unlocked it.
Any code present between a call to Lock and Unlock will be executed by only one Goroutine. If one Goroutine already has the lock and if a new Goroutine is trying to get the lock, then the new Goroutine will be stopped until the mutex is unlocked.
A normal mutex cannot be locked repeatedly by the owner. Attempts by a thread to relock an already held mutex, or to lock a mutex that was held by another thread when that thread terminated, cause a deadlock condition. A recursive mutex can be locked repeatedly by the owner.
I'll throw my preference out there - use the 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 }
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("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 }
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With