Now you either refactor into multiple functions, while ensuring all copies of possibly shared data when passing function arguments are correctly guarded or ”manually” unlock when you don’t need the mutex access anymore.
Now you either refactor into multiple functions, while ensuring all copies of possibly shared data when passing function arguments are correctly guarded or ”manually” unlock when you don’t need the mutex access anymore.
In rust you would just throw a block around the mutex access changing the scoping and ensuring it is dropped before the slow function is called.
Call it a minimally intrusive manual unlock.
drop(foo); // Now foo doesn't exist, it was dropped, thus unlocking anything which was kept locked while foo exists
If you feel that the name drop isn't helpful you can write your own function which consumes the guard, it needn't actually "do" anything with it - the whole point is that we moved the guard into this function, so, if the function doesn't return it or store it somewhere it's gone. This is why Destructive Move is the correct semantic and C++ "move" was a mistake.Specifically for Go, I'd try to address the problem in CSP style, so as to avoid explicit locks unless absolutely necessary.
Now for the case you mention, one can actually achieve the same in Go, it just takes a bit of prior work to set up the infra.
type Foo struct {sync.Mutex; s string}
func doLocked[T sync.Locker](data T, fn func(data T)) {
data.Lock(); defer data.Unlock(); fn(data)
}
func main() {
foo := &Foo{s: "Hello"}
doLocked(foo, func(foo *Foo) {
/* ... */
})
/* do the slow stuff */
}It’s absolutely a go-specific problem from defer being function scoped. Which could be ignored if Unlock was idempotent but it’s not.
This alleviates all these problems of unlocks within if bodies at the cost of an indent (and maybe slight performance penalty).