Common errors of go mutex lock

Common errors when using Mutex lock:

  • Non paired
  • copy a stateful Mutex
  • Lock reentry
  • deadlock

The following is a use case of an incorrect reentry lock:

func foo(l sync.Locker) {
    fmt.Println("in foo")
    l.Lock()
    bar(l)
    l.Unlock()
}


func bar(l sync.Locker) {
    l.Lock()
    fmt.Println("in bar")
    l.Unlock()
}


func main() {
    l := &sync.Mutex{}
    foo(l)
}

Design a reentrant Mutex lock
The key here is that the implemented lock should remember which goroutine currently holds the lock. There are two options:

  • Obtain the goroutine Id through the haker method (i.e. unconventional mode), and record the goroutine Id to obtain the lock, which can implement the Locker interface.
  • When calling Lock and Unlock methods, provide an additional token parameter to identify the goroutine that obtains the Lock, but this does not meet the Locker interface.

Scheme 1: goroutine id
There are two ways to obtain frame stack information. One is simple. You can obtain frame stack information through runtime.Stack, including goroutine id.

func GoID() int {
	var buf [64]byte
	n := runtime.Stack(buf[:], false)
	idField := strings.Fields(strings.TrimPrefix(buf[:n], "goroutine"))[0]
	id, err := strconv.Atoi(idField)
	if err != nil {
		panic(fmt.Sprintf("cannot get goroutine id: %v", err))
	}
	return id
}

Another way:
First, we get the g pointer of the runtime and inverse solve the structure of the corresponding g. The g pointer of each running goroutine structure is stored in a TLS object called the current goroutine. Step 1: we first get the TLS object; Step 2: obtain the g pointer of goroutine structure from TLS; Step 3: take the goroutine id from the g pointer.

It should be noted that the structure of goroutine in different Go versions may be different, so it needs to be adjusted according to different Go versions. Of course, if you want to find out the differences in the goroutine structure of various versions, the content involved is too low-level and complex, and the learning cost is too high. What shall I do? We can focus on some libraries. We don't need to invent the wheel repeatedly. We can directly use the third-party library to obtain the goroutine id. The good news is that there are many mature methods that can support multiple Go versions of goroutine id. I recommend a common library: petermattis/goid.

After obtaining the goroutine id, the next step is to design a reentrant Mutex lock.

func (m *RecursiveMutext) Lock() {
	gid := goid.Get()
	if atomic.LoadInt64(&m.owner) == gid { // If it's yourself
		m.recursion++
		return
	}
	m.Mutex.Lock()
	atomic.StoreInt64(&m.owner, gid)
	m.recursion = 1
}

func (m *RecursiveMutext) Unlock() {
	gid := goid.Get()
	if atomic.LoadInt64(&m.owner) != gid {
		panic(fmt.Sprintf("wrong the owner(%d): %d!", m.owner, gid))
	}
	m.recursion--
	if m.recursion != 0 {
		return
	}
	atomic.StoreInt64(&m.owner, -1)
	m.Mutex.Unlock()

Scheme 2: token

// Recursive lock in Token mode
type TokenRecursiveMutex struct {
    sync.Mutex
    token     int64
    recursion int32
}

// To request a lock, a token needs to be passed in
func (m *TokenRecursiveMutex) Lock(token int64) {
    if atomic.LoadInt64(&m.token) == token { //If the passed in token is consistent with the token holding the lock, it indicates that it is a recursive call
        m.recursion++
        return
    }
    m.Mutex.Lock() // The passed in token s are inconsistent, indicating that they are not recursive calls
    // Record this token after grabbing the lock
    atomic.StoreInt64(&m.token, token)
    m.recursion = 1
}

// Release lock
func (m *TokenRecursiveMutex) Unlock(token int64) {
    if atomic.LoadInt64(&m.token) != token { // Release the locks held by other token s
        panic(fmt.Sprintf("wrong the owner(%d): %d!", m.token, token))
    }
    m.recursion-- // The token that currently holds this lock releases the lock
    if m.recursion != 0 { // It has not yet fallen back to the original recursive call
        return
    }
    atomic.StoreInt64(&m.token, 0) // There are no recursive calls. Release the lock
    m.Mutex.Unlock()
}

Tags: Go

Posted on Sat, 20 Nov 2021 04:08:21 -0500 by suomynonA