Appearance
OS-Level Synchronization Primitives
Synchronization primitives are fundamental building blocks for concurrent programming. They ensure thread safety, prevent race conditions, and coordinate access to shared resources. Choosing the right synchronization primitive can make the difference between a high-performance application and one that's plagued by contention and deadlocks.
Mutexes
What are Mutexes?
A mutex (mutual exclusion) is a synchronization primitiveA synchronization primitive is a mechanism that allows threads to coordinate their actions, ensuring that only one thread can access a shared resource at a time that provides exclusive access to a shared resource. Only one thread can hold a mutex at a time, ensuring that critical sectionsA critical section is a section of code that requires coordination between threads. For example, in a banking system, you may want to ensure that no thread is withdrawing money while another thread is calling deposit and vice-versa. are executed atomicallyAn atomic operation is defined as an operation that happens as a single indivisible unit and can't be interrupted by another thread.
Semaphores
What are Semaphores?
A semaphore is a synchronization primitive that controls access to a finite pool of resources. Unlike mutexes, semaphores can allow multiple threads to access a resource simultaneously, up to a specified limit.
1. Binary Semaphore (Similar to Mutex)
cpp
#include <semaphore.h>
sem_t binary_semaphore;
void init_binary_semaphore() {
sem_init(&binary_semaphore, 0, 1); // Initial value = 1
}
void binary_semaphore_example() {
sem_wait(&binary_semaphore); // Decrement (P operation)
// Critical section - only one thread can execute this
shared_resource_operation();
sem_post(&binary_semaphore); // Increment (V operation)
}2. Counting Semaphore
Controls access to multiple resources. Locks the critical section when the semaphore is decremented all the way down to 0.
cpp
#include <semaphore.h>
#include <vector>
class ResourcePool {
private:
sem_t available_resources;
std::vector<int> resources;
std::mutex resource_mutex;
public:
ResourcePool(int pool_size) : resources(pool_size) {
sem_init(&available_resources, 0, pool_size);
for (int i = 0; i < pool_size; i++) {
resources[i] = i; // Initialize resources
}
}
int acquire_resource() {
sem_wait(&available_resources); // Wait for available resource
std::lock_guard<std::mutex> lock(resource_mutex);
int resource = resources.back();
resources.pop_back();
return resource;
}
void release_resource(int resource) {
std::lock_guard<std::mutex> lock(resource_mutex);
resources.push_back(resource);
sem_post(&available_resources); // Signal resource available
}
~ResourcePool() {
sem_destroy(&available_resources);
}
};
// Usage example
void worker_thread(ResourcePool& pool) {
int resource = pool.acquire_resource();
// Use the resource
printf("Thread %lu using resource %d\n",
std::this_thread::get_id(), resource);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
pool.release_resource(resource);
}Spinlocks
What are Spinlocks?
A spinlock is a synchronization primitive that causes a thread to wait in a loop while checking if a lock is available. Unlike mutexes, spinlocks don't cause the thread to sleep (and context switch), making them suitable for very short critical sections.
Spinlocks are essentially a busy-wait loop that checks if the lock is available. If it is, the thread acquires the lock and continues executing. If it is not, the thread spins in a loop, checking the lock status repeatedly.
cpp
while (lock_is_not_available); // Spin until the lock is availableMutexes
- Use when: You need exclusive access to a resource
- Best for: General-purpose synchronization
- Performance: Good for medium to long critical sections
- Overhead: Context switch when blocked
Semaphores
- Use when: You need to control access to multiple resources
- Best for: Producer-consumer patterns, resource pools
- Performance: Good for resource management
- Overhead: Context switch when blocked
Spinlocks
- Use when: Critical sections are very short (<1μs)
- Best for: High-frequency operations, interrupt handlers
- Performance: Excellent for short critical sections
- Overhead: CPU spinning (wastes CPU cycles)
Performance Considerations
Lock Contention Analysis
Locks can get highly contended when multiple threads are trying to access the same resource. This can lead to performance degradation and even deadlocks. It is imperative that critical sections are as short as possible. If the critical section is too long, the thread will be blocked for a long time, and other threads will be waiting for the lock to be released. This is called lock contention.
Also, if the critical sections are very short, it is wise to use a spinlock instead of a mutex. Spinlocks are better for very short critical sections because they don't cause the thread to sleep and context switch, which is a very expensive operation.
Lock-Free Alternatives
Lock-free algorithms allow you to avoid the overhead of context switching and mutexes. They are more complex to implement and understand, but they can offer better performance in some cases. We'll discuss these in the Low latency C++ roadmap.
Key Takeaways
Performance Characteristics:
- Mutexes: Good for general-purpose synchronization, context switch overhead
- Semaphores: Excellent for resource management, flexible counting
- Spinlocks: Best for very short critical sections, no context switch overhead
- Read-Write Locks: Optimal for read-heavy workloads
- Lock-free: Highest performance for simple operations
When to Use Each:
- Mutex: General synchronization, medium to long critical sections
- Semaphore: Resource pools, producer-consumer patterns
- Spinlock: Very short critical sections (<1μs), interrupt handlers
- Lock-free: Simple atomic operations, maximum performance
Performance Optimization:
- Minimize critical section size
- Use appropriate primitive for the workload
- Consider lock-free alternatives when possible
- Profile and measure contention
- Use CPU affinity to reduce cache misses