Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Attempting to acquire a lock with a deadline in golang?

Tags:

go

timeout

mutex

How can one only attempt to acquire a mutex-like lock in go, either aborting immediately (like TryLock does in other implementations) or by observing some form of deadline (basically LockBefore)?

I can think of 2 situations right now where this would be greatly helpful and where I'm looking for some sort of solution. The first one is: a CPU-heavy service which receives latency sensitive requests (e.g. a web service). In this case you would want to do something like the RPCService example below. It is possible to implement it as a worker queue (with channels and stuff), but in that case it becomes more difficult to gauge and utilize all available CPU. It is also possible to just accept that by the time you acquire the lock your code may already be over deadline, but that is not ideal as it wastes some amount of resources and means we can't do things like a "degraded ad-hoc response".

    /* Example 1: LockBefore() for latency sensitive code. */
    func (s *RPCService) DoTheThing(ctx context.Context, ...) ... {
      if s.someObj[req.Parameter].mtx.LockBefore(ctx.Deadline()) {
        defer s.someObj[req.Parameter].mtx.Unlock()
        ... expensive computation based on internal state ...
      } else {
        return s.cheapCachedResponse[req.Parameter]
      }
    }

Another case is when you have a bunch of objects which should be touched, but which may be locked, and where touching them should complete within a certain amount of time (e.g. updating some stats). In this case you could also either use LockBefore() or some form of TryLock(), see the Stats example below.

    /* Example 2: TryLock() for updating stats. */
    func (s *StatsObject) updateObjStats(key, value interface{}) {
      if s.someObj[key].TryLock() {
        defer s.someObj[key].Unlock()
        ... update stats ...
        ... fill in s.cheapCachedResponse ...
      }
    }

    func (s *StatsObject) UpdateStats() {
      s.someObj.Range(s.updateObjStats)
    }

For ease of use, let's assume that in the above case we're talking about the same s.someObj. Any object may be blocked by DoTheThing() operations for a long time, which means we would want to skip it in updateObjStats. Also, we would want to make sure that we return the cheap response in DoTheThing() in case we can't acquire a lock in time.

Unfortunately, sync.Mutex only and exclusively has the functions Lock() and Unlock(). There is no way to potentially acquire a lock. Is there some easy way to do this instead? Am I approaching this class of problems from an entirely wrong angle, and is there a different, more "go"ish way to solve them? Or will I have to implement my own Mutex library if I want to solve these? I am aware of issue 6123 which seems to suggest that there is no such thing and that the way I'm approaching these problems is entirely un-go-ish.

like image 956
A Child of The Universe Avatar asked Feb 01 '19 23:02

A Child of The Universe


3 Answers

Use a channel with buffer size of one as mutex.

l := make(chan struct{}, 1)

Lock:

l <- struct{}{}

Unlock:

<-l

Try lock:

select {
case l <- struct{}{}:
    // lock acquired
    <-l
default:
    // lock not acquired
}

Try with timeout:

select {
case l <- struct{}{}:
    // lock acquired
    <-l
case <-time.After(time.Minute):
    // lock not acquired
}
like image 195
Bayta Darell Avatar answered Oct 20 '22 18:10

Bayta Darell


I think you're asking several different things here:

  1. Does this facility exist in the standard libray? No, it doesn't. You can probably find implementations elsewhere - this is possible to implement using the standard library (atomics, for example).

  2. Why doesn't this facility exist in the standard library: the issue you mentioned in the question is one discussion. There are also several discussions on the go-nuts mailing list with several Go code developers contributing: link 1, link 2. And it's easy to find other discussions by googling.

  3. How can I design my program such that I won't need this?

The answer to (3) is more nuanced and depends on your exact issue. Your question already says

It is possible to implement it as a worker queue (with channels and stuff), but in that case it becomes more difficult to gauge and utilize all available CPU

Without providing details on why it would be more difficult to utilize all CPUs, as opposed to checking for a mutex lock state.

In Go you usually want channels whenever the locking schemes become non-trivial. It shouldn't be slower, and it should be much more maintainable.

like image 35
Eli Bendersky Avatar answered Oct 20 '22 17:10

Eli Bendersky


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 (
    "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
}
like image 30
Viney Avatar answered Oct 20 '22 17:10

Viney