C++ Thread Safety: Managing Shared Data with Mutexes and Atomics.

C++ Thread Safety: Managing Shared Data with Mutexes and Atomics.

Mutexes (Mutual Exclusion)

  • Purpose: Mutexes are synchronization primitives used to protect shared data in multi-threaded environments. They ensure that only one thread can access the critical section (protected code block) at a time, preventing data races and ensuring consistent results.
  • When to Use:Complex Operations: When multiple threads need to perform complex operations on shared data that cannot be atomically updated.Coarse-Grained Locking: When you want to protect a larger section of code or multiple variables with a single lock.
  • How to Use:std::mutex: The most basic mutex type.std::lock_guard: A RAII (Resource Acquisition Is Initialization) wrapper that automatically locks the mutex on construction and unlocks it on destruction, ensuring proper locking and unlocking even in the presence of exceptions.

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

std::mutex dataMutex;   // Global mutex to protect shared data
std::vector<int> data = {1, 2, 3};

void modifyData(int newValue) {
    std::lock_guard<std::mutex> lock(dataMutex); // Automatically locks and unlocks
    data.push_back(newValue);
}

int main() {
    std::thread t1(modifyData, 10);
    std::thread t2(modifyData, 20);
    
    t1.join();
    t2.join();

    for (int value : data) {
        std::cout << value << " "; // Output: 1 2 3 10 20
    }
    return 0;
}        

Atomics

  • Purpose: Atomics provide operations on shared data that are guaranteed to be indivisible (atomic). This means that an atomic operation completes without interference from other threads.
  • When to Use:Simple Operations: When you only need to perform basic operations on shared data, like incrementing a counter or updating a flag.Fine-Grained Locking: When you want to avoid the overhead of a mutex for simple updates and minimize contention between threads.
  • How to Use:std::atomic<T>: Template class for creating atomic variables of various types (e.g., std::atomic<int>, std::atomic<bool>).Atomic Operations: Use member functions like load, store, fetch_add, compare_exchange_weak, etc., for thread-safe access and modification.

#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> counter(0);  // Atomic counter

void incrementCounter() {
    for (int i = 0; i < 1000000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);  // Increment atomically
    }
}

int main() {
    std::thread t1(incrementCounter);
    std::thread t2(incrementCounter);

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

    std::cout << "Final counter value: " << counter << std::endl;  // Output: 2000000
    return 0;
}        

Choosing Between Mutexes and Atomics

  • Simplicity vs. Performance: Mutexes are simpler to use but may introduce more overhead due to locking. Atomics are more efficient for simple operations, but require careful consideration of memory ordering to ensure correctness in complex scenarios.
  • Granularity: Mutexes are suitable for protecting larger critical sections, while atomics are ideal for fine-grained synchronization of individual variables.

Cristian Castro

C C++ Developer ,Django, Devops,EOSIO BlockChain Developer

10 个月

great article

要查看或添加评论,请登录

Ayman Alheraki的更多文章

社区洞察

其他会员也浏览了