Suppose I have an application that may or may not have spawned multiple threads. Is it worth it to protect operations that need synchronization conditionally with a std::mutex as shown below, or is the lock so cheap that it does not matter when single-threading?
#include <atomic>
#include <mutex>
std::atomic<bool> more_than_one_thread_active{false};
void operation_requiring_synchronization() {
//...
}
void call_operation_requiring_synchronization() {
if (more_than_one_thread_active) {
static std::mutex mutex;
std::lock_guard<std::mutex> lock(mutex);
operation_requiring_synchronization();
} else {
operation_requiring_synchronization();
}
}
Edit
Thanks to all who have answered and commented, very interesting discussion.
A couple of clarifications:
The application processes chunks of input, and for each chunk decides if it will be processed in a single-threaded or parallel or otherwise concurrent fashion. It is not unlikely that no multi-threading will be needed.
The operation_requiring_synchronization()
will typically consist of a few inserts into global standard containers.
Profiling is, of course, difficult when the application is platform-independent and should perform well under a variety of platforms and compilers (past, present and future).
Based on the discussion so far, I tend to think that the optimization is worth it.
I also think the std::atomic<bool> more_than_one_thread_active
should probably be changed to a non-atomic bool multithreading_has_been_initialized
. The original idea was to be able to turn the flag off again when all threads other than the main one are dormant but I see how this could be error-prone.
Abstracting the explicit conditional away into a customized lock_guard is a good idea (and facilitates future changes of the design, including simply reverting back to std::lock_guard if the optimization is not deemed worth it).
If one or more threads are waiting to lock the mutex, pthread_mutex_unlock() causes one of those threads to return from pthread_mutex_lock() with the mutex object acquired. If no threads are waiting for the mutex, the mutex unlocks with no current owner.
Locking unlocked mutex is really cheap.
atomic integer is a user mode object there for it's much more efficient than a mutex which runs in kernel mode.
Shared mutexes and locks are an optimization for read-only pieces of multi-threaded code. It is totally safe for multiple threads to read the same variable, but std::mutex can not be locked by multiple threads simultaneously, even if those threads only want to read a value.
Generally, optimizations should not be performed in the absence of demonstrated need in your specific use case if they affect the design or organization of code. That's because these kinds of algorithmic optimizations can be very difficult to perform later. Point micro-optimizations can always be added later and should be avoided prior to need for several reasons:
If you guess wrong about the typical use case, they can actually make performance worse.
They can make code harder to debug and maintain.
Even if you guess right about the use case, they can make performance worse on new platforms. For example, mutex acquisition has gotten more than an order of magnitude cheaper in the last eight years. Tradeoffs that make sense today might not make sense tomorrow.
You can wind up wasting time on things that are unnecessary, and worse you can waste time that needed to go into other optimizations. Without enormous amounts of experience, it's very difficult to predict where the actual bottlenecks in your code will be, and even experts are frequently surprised when they actually profile.
This is a classic point micro-optimization, so it should be done only if profiling demonstrates some likely benefit.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With