0

What would the appropriate design pattern to avoid deadlock when several functions use the same mutex ?

It is quite easy to forget what method uses the lock and so it happens that you call a function that do a mutex.Lock() inside a function that already locked the mutex --> deadlock.

For example in the following code, it is not explicit right away that the setToNextEvenNb() is a deadlock. You have to look in nested functions. And it would be even worse if the Lock was in function called 2 or 3 level behind the setToNextEvenNb().

package main

import (
    "fmt"
    "sync"
)

type data struct {
    value int
    mutex sync.RWMutex
}

func (d *data) isEven() bool {
    d.mutex.RLock()
    defer d.mutex.RUnlock()
    return d.value%2 == 0
}

func (d *data) setToNextEvenNb() {
    d.mutex.Lock()
    defer d.mutex.Unlock()
    if d.isEven() {
        d.value += 2
    }
    d.value += 1
}

func (d *data) set(value int) {
    d.mutex.Lock()
    defer d.mutex.Unlock()
    d.value = value
}

func main() {
    var d data
    d.set(10)
    fmt.Println("data", d.value)
    fmt.Println("is even ?", d.isEven())
    d.setToNextEvenNb()
    fmt.Println("data", d.value)
}

When you have a large code base, this can happen quite easily. I feel that there should be a design pattern to avoid this sort of scenario and I was looking for any advice on this ?

cylon86
  • 550
  • 4
  • 20
  • Don't use mutexes. Use channels. This is the best solution. – Chayim Friedman Jul 20 '23 at 15:38
  • 1
    possible duplicate? https://stackoverflow.com/q/14670979/801894 – Solomon Slow Jul 20 '23 at 15:47
  • 1
    @ChayimFriedman channels are amazing, but mutex exists for a reason: they are sometimes better fit to some situation (https://stackoverflow.com/questions/47312029/when-should-you-use-a-mutex-over-a-channel) – cylon86 Jul 20 '23 at 15:50
  • @SolomonSlow thanks for the reference very interesting reading. But I agree, I don't want to implement recursive mutex. My question is how do you make sure that in your code you didn't implemented one by mistake ? – cylon86 Jul 20 '23 at 16:02
  • 1
    @cylon86, I think most users who naively ask for recursive mutexes are just hoping to avoid this same question, so the top answer there is still applicable. You will often see public/private entrypoints which are locked/unlocked respectively. The rules are then easy to follow, external calls use the public method, and calls within methods use the private version. The public version only needs to wrap the private version with a lock. – JimB Jul 20 '23 at 16:07
  • @JimB yes so that's a design pattern, no lock inside public function. But it doesn't solve the issue: it is quite usual to call another private method from within a private method which can end up being a deadlock because they lock the same mutex. I feel that there should be something better than "having to check the implementation of all functions called below a `Lock()`" because it's too easy to forget so it will not stand long... – cylon86 Jul 20 '23 at 16:18
  • 2
    Re, "How do you make sure...you didn't implement one by mistake." Let's get some terminology straight first. A _recursive mutex_ (a.k.a., [_reentrant mutex_](https://en.wikipedia.org/wiki/Reentrant_mutex)) is a special kind of mutex that can be locked more than one time by the same thread _without_ causing a self-deadlock. The question is not, how could you design and implement that by mistake. The question is, how can you write code that doesn't ever _need_ a recursive mutex. (I.E., doesn't ever need a single thread to lock the same mutex multiple times.) – Solomon Slow Jul 20 '23 at 16:31
  • @SolomonSlow yes that's the question :) – cylon86 Jul 20 '23 at 16:36
  • 1
    I don't know of a simple answer. But part of the answer is, keep critical sections as small as possible. When writing single-threaded code, I always strive to write the least number of lines of code that can get the job done. But when I'm writing multi-threaded code, I often am willing to write many _extra_ lines of code that lie outside of any critical section if it lets me get rid of one line of code from within a critical section—especially true, if the one line that I'm getting rid of is a function call. – Solomon Slow Jul 20 '23 at 16:44
  • 2
    @cylon86, the point of that pattern is that the private methods don’t handle the mutex at all so there is no chance for deadlock unless you use the wrong entry point which is easier to spot. They don’t need to be public/private, you could use some other naming convention, but the strategy is to decouple the locks from the single-threaded logic – JimB Jul 20 '23 at 16:51
  • 1
    This question is arguably not appropriate for SO since it involves a lot of subjectivity and entire chapters of books are dedicated to the topic. The issue is also not unique to Go. You'll have the same problem with any language without reentrant/recursive locks or where you choose not to use them. – Kurtis Rader Jul 21 '23 at 03:10

0 Answers0