Recursive (Re-Entrant) Locks (2013)
blog.stephencleary.com
blog.stephencleary.com
https://groups.google.com/g/comp.programming.threads/c/tcrTK...
lock(something) when recursive will do one or two things: either aquire the lock or simply validate that it’s held.
Is there a different pattern that simply allows validating in C_UnderLock that the lock is in fact held?
so in A and B you do lock(_mutex) but in C you do assert_lock(_mutex) which either panics or continues? What would that look like?
For example let's say you have a class Resource that needs to be locked and a method Foo(Resource) that needs to operate on the resource under a lock.
You could change Foo(Resource) to Foo(LockedResource). LockedResource could be a class that is defined using a lock object and a resource and takes the lock or validates that lock is taken in the constructor.
Then by making the object disposable the release can be handled as well.
This way the type system handles the validation.
This is kinda like a budget version of having the type system support locking and concurrency like Rust does.
Make "lock being held" into a resource, which can then be passed down to callees to assert (at compile-time) that a lock is being held. Even better if the lock itself is associated with (and lends on locking) values, then just having access to the protected resource indicates that you hold the lock.
Obviously without language support for ownership and borrowing this has limited safety as you can stash a lock token and reuse it later, despite not holding the lock. Then again, even in Rust if you really wanted to fuck someone's day you could probably `transmute` garbage into a lock token.
There are also patterns like the synchronized wrapper were the lock is bolted on transparently and all the functions in a class can call each other without worrying about the lock.
What's wrong with doing exactly what you said? If somebody ends up misusing C_UnderLock, their program will crash, and they can go fix their bug.
if (!Monitor.IsEntered(_x)) throw new InvalidOperation.
Will do the trick in C#
Compare and swap still typically requires the cache line to bounce around between cores, so if that’s the primary cost of locks for you it seems like compare and swap doesn’t really fix the problem of frequent synchronization between cores.
This may sound like some nightmare architecture, but I ended up in this situation when writing a simple thread-safe malloc override using LD_PRELOAD.
Otherwise, the only thing I could think of is a thread-local boolean to tell 'F' that it is being called from 'I'. Which is, as I understand it, basically what a recursive mutex does, by remembering the thread that locked it. But it's still technically a different construct. The tricky part of the problem under this condition is that only the recursive call of 'F' from 'I' can proceed, so somehow that call must be distinguished from an identical call to 'F' from a different thread.
Overall, things are much trickier, especially in multithreaded code, when you can't directly thread state through your functions. If there's one point I'll give in favor of the FP philosophy, it's that.
I assume the issue is you can't (easily) change I (for example F is malloc). Then any solution in practice would look like a recursive mutex.
That's alright, recursive mutexes are bad, but one legitimate use case is retrofitting thread safety on top on a non thread safe interface (infamously xlib was made thread safe by just bolting on a global recursive mutex).
* ClassA conforms to a protocol that allows it to return its description as a string
* The protocol includes a regular `description` function as well as an optional `debugDescription` function
* An object conforming to this protocol can be passed to a global logging function, which then calls `description` or `debugDescription` depending on whether the debugger is attached or not
* ClassA's state is protected by a lock
* ClassA's description implementation goes through its state and converts it to a string representation
* Accordingly, ClassA's description implementation needs to also be locked
* The global logging function can be called from within ClassA's state lock, and can take the current instance of ClassA as a parameter
The recommended approach of making `description` call `description_underLock` doesn't work, because the logging function decides whether to invoke `description` or `debugDescription` depending on the context. In other words, if the logging function is called from inside the state lock with `self` as an argument, then we'll get a deadlock if we don't manually invoke `description_underLock`; but if we directly give the logging function the string from `description_underLock` or `debugDescription_underLock` instead of `self`, we now have to replicate the internal logic of the logging function in our layer. Alternatively, we can impose conditions such as "never log `self` from within the state lock," but that feels arbitrary and restrictive.
I think recursive locks make sense in this case, and probably other cases where protocols and/or indirection is involved. We just have to make sure that our description implementation is dead simple (mostly a "leaf node" function) and doesn't have side effects.
I'm sorry but that's literally advocating for spaghetti code. If the author really wrote a lot of real world code, it doesn't look like it was clean or well structured.
I took some time to refactor it all to not use recursive locks, instead having a clean boundary between internal (non-locking) methods and external methods which did the locking and called the internal ones.
Not only did our locking issues go away immediately, it lead to a much simpler developer experience since there was never any question if this new function needed to lock or not etc.
I've not written that heavy multi-threaded code since, but my takeaway from the experience was that requiring recursive locks was a sign of poor design.
Instead of one "public" function call resulting in locking the same lock recursively 4-5 times as the "public" function called other functions and so on, a single locking operation was done at the boundary.
It's the opposite of spaghetti code.
This bit me in Rust when I was goofing off / exploring with one of the AOC'22 puzzles back in December. One of Rust's selling points is supposedly "fearless concurrency" but it turns out deadlocking is trivial to do: did you know a mutex is not re-entrant in rust? Just acquire the lock again in a nested function and blam, at runtime when you reach that codepath you'll learn you have a deadlock.
I'll probably never stop trying new approaches, I love exploring languages for how they can open your eyes but so far Clojure's approach to this problem - by that I mean immutable everything by default and then using atom's, I'm not talking about the richer STM stuff, I haven't played with that yet - is peak concurrency handling in the context of writing business applications.
[1] As I wrote this the 'dynamic' type jumped into my mind, would its presence mean its technically wrong to say c# is statically typed? It's not "only" statically typed I guess is what I'm uncomfortable with.