1

I have a recursive function that I will lock and mutate its internal state, however, it causes a deadlock. How can I implement such a recursive function without deadlock?

package main

import (
    "fmt"
    "sync"
)

type runner struct {
    sync.Mutex
    value int
}

func (r *runner) decrement() {
    r.Lock()
    defer r.Unlock()
    if r.value == 0 {
        return
    }
    r.value--
    r.decrement()
}

func main() {
    r := runner{
        value: 10,
    }

    r.decrement()

    fmt.Printf("r: %v\n", r.value)
}

Expect running the above code would print r: 0, but actually got deadlock:

fatal error: all goroutines are asleep - deadlock!
Leo Zhang
  • 3,040
  • 3
  • 24
  • 38
  • To suggest you a solution you first need to explain the problem. Just looking at this code the answer would be: you don't need locks here at all. – zerkms Oct 31 '19 at 21:45
  • `sync.Mutex` is not reentrant – JimB Oct 31 '19 at 21:47
  • 2
    Possible duplicate of [Recursive locking in Go](https://stackoverflow.com/questions/14670979/recursive-locking-in-go) – nauman Oct 31 '19 at 21:50

1 Answers1

1

The typical solution to this kind of problem is a reentrant mutex. However, Russ Cox of the Go team has a good argument as to why reentrant mutexes are a bad idea.

In this case, you don't want to defer your unlock. Instead, you should lock and unlock the minimal sections necessary.

func (r *runner) decrement() {
    r.Lock()
    if r.value == 0 {
        r.Unlock()
        return
    }
    r.value--
    r.Unlock()
    r.decrement()
}

This solves two problems: one, it (marginally) improves the ability of your code to run concurrently by not taking locks for things which don't need locking, and two, it ensures that if you re-enter decrement(), there isn't an outstanding lock that will result in a deadlock.

Chris Heald
  • 61,439
  • 10
  • 123
  • 137
  • The same as the other answer: all `r.value` accesses must be synchronised otherwise it's a data race. – zerkms Oct 31 '19 at 22:07
  • And if this code is intended to be concurrently (otherwise why locks at all) 2 concurrent calls may turn it into an infinite recursion. – zerkms Oct 31 '19 at 22:13
  • Ah, of course, I was thinking that reads were atomic, for some reason. – Chris Heald Oct 31 '19 at 22:22