tooptoop4
tooptoop4

Reputation: 330

Golang - lock per value

I am writing a golang api that accepts a tableName value and a updEpoch value, ie:

curl -F "tableName=abc" -F "updEpoch=123" myhost:8080/singleroute
curl -F "tableName=abc" -F "updEpoch=456" myhost:8080/singleroute
curl -F "tableName=def" -F "updEpoch=123" myhost:8080/singleroute
curl -F "tableName=def" -F "updEpoch=345" myhost:8080/singleroute

I want to allow multiple different tableName requests to be handled in parallel BUT only 1 request per tableName at the same time. So in above example, if above 4 requests fired at same time, then 1st and 3rd should be able to run at same time (as unique tableNames), but 2nd will only start once 1st finishes and 4th will only start once 3rd finishes. When I was researching mutex no example seemed to fit this case, I don't want to hardcode abc/def.etc anywhere in the code as same rule should apply to any arbitrary tableName.

my guess based on Crowman's help:

package main

import (
    "fmt"
    "sync"
    "time"
    "http"
)
km := KeyedMutex{}
type KeyedMutex struct {
    mutexes sync.Map // Zero value is empty and ready for use
}

func (m *KeyedMutex) Lock(key string) func() {
    value, _ := m.mutexes.LoadOrStore(key, &sync.Mutex{})
    mtx := value.(*sync.Mutex)
    mtx.Lock()

    return func() { mtx.Unlock() }
}

func myFunc(key string, data string) string {
  //do some stuff
  return "done for key:"+key+", data: "+data
}

func main() {
    key := //some form value sent to my api 
    data := //some form value sent to my api 
    unlock := km.Lock(key)
    defer unlock()
    retVal := myFunc(key, data)
}

Upvotes: 13

Views: 7014

Answers (3)

user27745807
user27745807

Reputation: 1

An update on @Crowman 's answer:

package utils

import "sync"

type KeyedMutex struct {
    mutexes sync.Map // Zero value is empty and ready for use
}

func (m *KeyedMutex) Lock(key string) func() {
    value, _ := m.mutexes.LoadOrStore(key, &sync.Mutex{})
    mtx := value.(*sync.Mutex)
    mtx.Lock()

    return func() {
        mtx.Unlock()
        m.mutexes.Delete(key)
    }
}

func NewLock() KeyedMutex {
    return KeyedMutex{}
}

just adding a key-removing code, hope it will be helpful.

Upvotes: 0

Alexander Trakhimenok
Alexander Trakhimenok

Reputation: 6268

You can create a package level var map[string]*sync.Mutex and lock corresponding mutex that you will get/create by table name.

Upvotes: 0

Crowman
Crowman

Reputation: 25908

You can use a sync.Map with your table name as a key and a *sync.Mutex as the value.

For example:

package main

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

type KeyedMutex struct {
    mutexes sync.Map // Zero value is empty and ready for use
}

func (m *KeyedMutex) Lock(key string) func() {
    value, _ := m.mutexes.LoadOrStore(key, &sync.Mutex{})
    mtx := value.(*sync.Mutex)
    mtx.Lock()

    return func() { mtx.Unlock() }
}

func main() {
    wg := sync.WaitGroup{}
    km := KeyedMutex{}

    for _, job := range []struct {
        key  string
        data string
    }{
        {key: "abc", data: "123"},
        {key: "abc", data: "456"},
        {key: "def", data: "123"},
        {key: "def", data: "456"},
    } {
        var job = job
        wg.Add(1)

        go func() {
            defer wg.Done()

            unlock := km.Lock(job.key)
            defer unlock()

            fmt.Printf("%s:%s mutex acquired\n", job.key, job.data)
            time.Sleep(time.Second * 1) // To ensure some goroutines visibly block
            fmt.Printf("%s:%s done\n", job.key, job.data)
        }()
    }

    wg.Wait()
}

with sample output:

crow@mac:$ ./mut
def:456 mutex acquired
abc:456 mutex acquired
abc:456 done
def:456 done
abc:123 mutex acquired
def:123 mutex acquired
def:123 done
abc:123 done
crow@mac:$

to show that requests with distinct table names acquire a mutex immediately, but requests with the same table name are serialized.

Upvotes: 22

Related Questions