← Back to context

Comment by ViewTrick1002

5 days ago

Until you have to call a slow function after the mutex access leading to the lock being held long enough to cause problems.

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.

OK, but you're not in "Go"-specific problems any more, that's just concurrency issues. There isn't any approach to concurrency that will rigorously prevent programmers from writing code that doesn't progress sufficiently, not even going to the extremes of Erlang or Haskell. Even when there are no locks qua locks to be seen in the system at all I've written code that starved the system for resources by doing things like trying to route too much stuff through one Erlang process.

  • I would say it is a Go specific problem with how mutexes and defer are used together.

    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.

    • In Rust you can also explicitly drop the guard.

          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.

      1 reply →

    • Generally, in any language, I'd suggest of you're fiddling with lots of locks (be they mutexes, or whatever), then one is taking the wrong approach.

      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 */
        }

  • > OK, but you're not in "Go"-specific problems any more, that's just concurrency issues.

    It’s absolutely a go-specific problem from defer being function scoped. Which could be ignored if Unlock was idempotent but it’s not.

    • It's tedious, I agree, but I found it easiest to just wrap it in an inline function defined and called there and then.

      This alleviates all these problems of unlocks within if bodies at the cost of an indent (and maybe slight performance penalty).