The POSIX threads API for parallel programming uses mutexes to synchronize between threads. When code running on one thread wants to do something like modify a shared resource, it will call pthread_lock() on a pthread_mutex_t struct. If code on another thread got there first and already called pthread_lock() on the same mutex struct, then the call will block until that other thread calls pthread_unlock() on the struct after it's done modifying a shared resource.
The docs warn that if the mutex struct isn't initialized first before you try to lock it, the behavior is undefined. Similarly, at the end, when it comes time to clean up and tear down the mutex struct:
The behavior is undefined if the value specified by the
mutexargument topthread_mutex_destroy_()does not refer to an initialized mutex.
The docs also say:
A destroyed mutex object can be reinitialized using
pthread_mutex_init_(); the results of otherwise referencing the object after it has been destroyed are undefined.
And for good measure:
Attempting to initialize an already initialized mutex results in undefined behavior.
So. It seems like a Really Good Idea™ to check if that mutex is inited before touching it.
And yet...how to do that?
- It's a struct, not a pointer, so you can't compare to
NULL. - There is no function in the spec to test if the struct is initialized.
- You can't look inside the struct either.
No, really. Further down in the docs there is a "Rationale" section which explains that the pthread_mutex_t struct type is intentionally opaque and unspecified. They don't even want to get into what it would mean for two of them to be equal to each other, or what actually gets assigned during initialization. Different platforms are supposed to feel free to make whatever decisions make the most sense to them, right down to whether to use the heap or the stack--that's why it's a struct, so that if implementations want to, they can just put a pointer inside:
This volume of POSIX.1-2017 supports several alternative implementations of mutexes. An implementation may store the lock directly in the object of type
pthread_mutex_t. Alternatively, an implementation may store the lock in the heap and merely store a pointer, handle, or unique ID in the mutex object. Either implementation has advantages or may be required on certain hardware configurations. So that portable code can be written that is invariant to this choice, this volume of POSIX.1-2017 does not define assignment or equality for this type, and it uses the term "initialize" to reinforce the (more restrictive) notion that the lock may actually reside in the mutex object itself.
Note that this precludes an over-specification of the type of the mutex or condition variable and motivates the opaqueness of the type.
They do encourage implementations to fail gracefully if you try to destroy, lock, or unlock an uninitialized mutex, and return an EINVAL error, but it's just a recommendation.
And when someone proposed using that to detect it on Stack Overflow, they were told:
No you can't detect that.
EINVALmay be returned for an uninitialized mutex, but it mustn't necessarily. https://stackoverflow.com/questions/24468470/check-if-pthread-mutex-is-initialized
...and the original poster updated to say that person was right and when they tried their program would intermittently crash.
That SO discussion is focused on initialization, not destruction. It recommends using an approach discussed deep in the documentation, where you either use a pthread_once() call to guarantee a mutex is always initialized exactly one time, or a macro they provide for static initialization. The discussion ends with someone pointing out that even when you use that macro, you still want to eventually destroy your mutex structs.
From that and other sources I ran across, it seems like most people avoid this problem by centralizing the responsibility for the mutex with the thread pool owner. But in my case, I don't own the thread pool. I get instantiated on each worker thread in the pool. The system I'm operating inside of is very modular, and that means there are initialization and processing function signatures the worker must adhere to, those signatures are in use and expected across many modules I'm not currently touching, and in turn I am constrained from making changes like just passing a centrally-owned mutex at init or during the runloop.
This is the fun of C programming and spec-based APIs that date back decades. You can code yourself all the way into an architecture where your workers coordinate on managing their own mutex, and it works fine until you run into an edge case with a signal interrupt, and fixing that one edge case requires rewriting and relocating a bunch of code.
Normal vs Default
As another fun thing, pthreads has the concept of mutex type:
- Normal
- Error-Checking
- Recursive, or
- Default
Default isn't Normal, because a Normal mutex deadlocks if you lock it twice, whereas a Default mutex might deadlock, or it might gracefully error, or it might let you lock it multiple times, or it might engage in undefined behavior.
As summed up for Slack
i don't think i've ever taken a critical look at posix threading before. omg it's terrible.
there's an incredible number of different ways to end up with undefined or poorly specified behavior. a 'normal' mutex is supposed to deadlock if it's locked more than once. it's intentionally supposed to make your program hang. that's normal. but they also have 'default' mutexes. which aren't normal, even though, as the default, they're normally what you get. and a default mutex might deadlock if you lock it twice. or it might just return an error. or it might let you lock it a second time, and then expect to be unlocked twice to open again. or…it might do something completely undefined. naturally, all of this can happen regardless of whether or not you tell it to make the mutex 'robust', which is supposed to prevent undefined behavior--but only a different kind of it. the entire spec is like this. 