Mutexes, condition variables, semaphores in C++

Synchronization and locks are essential concepts in concurrent programming, which involves multiple threads of execution accessing shared resources concurrently. C++ provides several synchronization primitives, such as mutexes, condition variables, and semaphores, to manage access to shared resources and ensure thread safety.

Mutexes:

A mutex, short for “mutual exclusion,” is a synchronization primitive that provides mutual exclusion to shared resources by allowing only one thread to access the resource at a time. In C++, mutexes are implemented as objects of the `std::mutex` class. Here’s an example of how to use a mutex to protect access to a shared counter:

#include 
#include 
#include 

std::mutex mtx; // Mutex to protect counter
int counter = 0; // Shared counter

void increment() {
    for (int i = 0; i < 1000000; i++) {
        mtx.lock(); // Lock the mutex
        counter++;
        mtx.unlock(); // Unlock the mutex
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Counter value: " << counter << std::endl;
    return 0;
}

In this example, we define a mutex `mtx` to protect access to the shared counter `counter`. We then define a function `increment()` that uses a `for`loop to increment the counter 1 million times, while locking and unlocking the mutex to ensure mutual exclusion. We create two threads `t1` and `t2` that both call the `increment()` function, and then join the threads to wait for them to finish. Finally, we print the value of the counter.

Condition variables:

A condition variable is a synchronization primitive that allows threads to wait for a certain condition to be signaled by another thread. In C++, condition variables are implemented as objects of the `std::condition_variable` class. Here's an example of how to use a condition variable to coordinate access to a shared resource:

#include 
#include 
#include 
#include 

std::mutex mtx; // Mutex to protect shared resource
std::condition_variable cv; // Condition variable to signal changes to shared resource
bool ready = false; // Shared resource

void worker() {
    std::unique_lock lock(mtx); // Lock the mutex
    cv.wait(lock, []{ return ready; }); // Wait for ready to be true
    std::cout << "Worker thread: resource is ready!" << std::endl;
}

int main() {
    std::thread t1(worker);
    std::this_thread::sleep_for(std::chrono::seconds(1)); // Simulate work
    {
        std::lock_guard lock(mtx); // Lock the mutex
        ready= true; // Set the shared resource to ready
    }
    cv.notify_one(); // Signal the condition variable
    t1.join();
    return 0;
}

In this example, we define a mutex `mtx` to protect access to the shared resource `ready`, and a condition variable `cv` to signal changes to the resource. We define a function `worker()` that waits for `ready` to be set to `true` using the `cv.wait()` function, which atomically unlocks the mutex and waits for the condition variable to be signaled, and then relocks the mutex when the condition is met. We create a thread `t1` that calls the `worker()` function, and then set `ready` to `true` after a 1-second delay using a lock guard to ensure mutual exclusion. We then signal the condition variable using the `cv.notify_one()` function, which wakes up one thread waiting on the condition variable. Finally, we join the thread and exit the program.

Semaphores:

A semaphore is a synchronization primitive that allows multiple threads to access a shared resource with a limited capacity. In C++, semaphores are implemented as objects of the `std::counting_semaphore` or `std::binary_semaphore` classes, depending on the type of semaphore needed. Here's an example of how to use a counting semaphore to limit access to a shared resource:

#include 
#include 
#include 

std::counting_semaphore<2> sem(2); // Semaphore with a capacity of 2
int counter = 0; // Shared counter

void increment() {
    sem.acquire(); // Acquire a permit from the semaphore
    counter++;
    sem.release(); // Release the permit back to the semaphore
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    std::thread t3(increment);
    t1.join();
    t2.join();
    t3.join();
    std::cout << "Counter value: " << counter << std::endl;
    return 0;
}

In this example, we define a counting semaphore `sem` with a capacity of 2, which limits access to the shared resource `counter`. We define a function `increment()` that uses the `sem.acquire()` function to acquire a permit from the semaphore, increment the counter, and then release the permit using the `sem.release()` function. We create three threads `t1`, `t2`, and `t3` that all call the `increment()` function, but only two threads can access the resource at a time due to the capacity of the semaphore. Finally, we print the value of the counter.

Overall, synchronization and locks are essential concepts in concurrent programming, and C++ provides several synchronization primitives, such as mutexes, condition variables, and semaphores, to manage access to shared resources andensure thread safety. It's important to use these primitives correctly and effectively to avoid race conditions, deadlocks, and other synchronization issues that can arise in concurrent programs. When using mutexes, condition variables, and semaphores, it's important to ensure that they are properly initialized, acquired and released in the correct order, and used consistently across all threads that access the shared resource. It's also important to choose the appropriate synchronization primitive for the specific use case, based on factors such as the type of shared resource, the number of threads accessing the resource, and the desired synchronization behavior. With careful design and implementation, synchronization and locks can help you write robust, efficient, and scalable concurrent programs in C++.