Swiftorial Logo
Home
Swift Lessons
Tutorials
Learn More
Career
Resources

Mutexes and Locks in C++

Introduction

In multithreaded applications, it is common to encounter situations where multiple threads need to access shared resources. To prevent data races and ensure data consistency, mutexes (mutual exclusions) and locks are used. This tutorial will cover the basics of mutexes and locks in C++, explaining their usage with detailed examples.

What is a Mutex?

A mutex is a synchronization primitive that is used to protect shared resources from being simultaneously accessed by multiple threads. When a thread locks a mutex, other threads that try to lock the same mutex are blocked until the mutex is unlocked.

Using Mutexes in C++

C++11 introduced the std::mutex class in the <mutex> header, making it easier to work with mutexes. Below is an example of how to use std::mutex in C++:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx; // Mutex declaration

void print_thread_id(int id) {
    mtx.lock(); // Lock the mutex
    std::cout << "Thread " << id << std::endl;
    mtx.unlock(); // Unlock the mutex
}

int main() {
    std::thread t1(print_thread_id, 1);
    std::thread t2(print_thread_id, 2);

    t1.join();
    t2.join();

    return 0;
}
                

In this example, the mutex mtx is used to ensure that the std::cout stream is accessed by only one thread at a time, preventing mixed output from multiple threads.

Locks in C++

While manually locking and unlocking mutexes works, it is prone to errors, especially if exceptions are thrown. C++11 provides the std::lock_guard and std::unique_lock classes to manage mutexes more safely and conveniently.

Using std::lock_guard

The std::lock_guard is a simple way to manage a mutex. It locks the mutex upon creation and unlocks it when it goes out of scope. Here's an example:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx; // Mutex declaration

void safe_print(int id) {
    std::lock_guard<std::mutex> lock(mtx); // Lock the mutex
    std::cout << "Thread " << id << std::endl;
}

int main() {
    std::thread t1(safe_print, 1);
    std::thread t2(safe_print, 2);

    t1.join();
    t2.join();

    return 0;
}
                

In this example, the std::lock_guard automatically locks the mutex when it is created and unlocks it when it is destroyed, ensuring exception safety.

Using std::unique_lock

The std::unique_lock provides more flexibility compared to std::lock_guard. It allows deferred locking, timed locking, and manual unlocking. Here's an example:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx; // Mutex declaration

void flexible_print(int id) {
    std::unique_lock<std::mutex> lock(mtx); // Lock the mutex
    std::cout << "Thread " << id << std::endl;
    // Mutex will be unlocked when lock goes out of scope
}

int main() {
    std::thread t1(flexible_print, 1);
    std::thread t2(flexible_print, 2);

    t1.join();
    t2.join();

    return 0;
}
                

In this example, std::unique_lock is used to control the locking and unlocking of the mutex more flexibly.

Best Practices

When using mutexes and locks, follow these best practices to ensure safe and efficient multithreading:

  • Always lock the mutex before accessing shared resources.
  • Prefer std::lock_guard or std::unique_lock over manual locking and unlocking.
  • Avoid holding a lock for longer than necessary.
  • Be mindful of deadlocks and use lock hierarchies or std::lock to avoid them.
  • Consider using higher-level synchronization mechanisms like condition variables if appropriate.

Conclusion

Mutexes and locks are essential tools for ensuring safe access to shared resources in multithreaded applications. By understanding and using std::mutex, std::lock_guard, and std::unique_lock, you can write more robust and thread-safe C++ code. Remember to follow best practices to avoid common pitfalls like deadlocks and race conditions.