Hands-On Multithreading with C++ 02—Synchronization
Mutex, Atomic Operation, & Condition Variable
Complete code: https://github.com/yu-cheng-kuo-28/multithreading-cpp
You may refer to the technical article below for theoretical knowledge:
Continuing from the last article, which utilizes Mutex, let’s delve into the major synchronization usages and approaches. Also, you may check out my course notes on synchronization from online OS course delivered by PKU.
Outline
(1) Race Condition & Solution
(2) Synchronization Approaches: Mutex, Atomic Operation, & Condition Variable
(3) References
(1) Race Condition & Solution
1–1 Race condition
// 6_raceCondition_01.cpp
#include <iostream>
#include <thread>
#include <chrono>
int sharedVariable = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
int currentValue = sharedVariable;
std::this_thread::sleep_for(std::chrono::microseconds(1)); // Introduce a delay
sharedVariable = currentValue + 1;
}
}
int main() {
std::thread thread1(increment);
std::thread thread2(increment);
thread1.join();
thread2.join();
std::cout << "Expected value: 200000" << std::endl;
std::cout << "Actual value: " << sharedVariable << std::endl;
return 0;
}
The output:
root@DemoYuChengKuo:~/3_multithreading_cpp/code_02_synchronization# ./6_raceCondition_01
Expected value: 200000
Actual value: 100000
1–2 Solution to the race condition example: Mutex
// 7_raceCondition_02.cpp
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
int sharedVariable = 0;
std::mutex mtx; // Mutex for synchronization
void increment() {
for (int i = 0; i < 100000; ++i) {
mtx.lock(); // Lock the mutex
int currentValue = sharedVariable;
std::this_thread::sleep_for(std::chrono::microseconds(1)); // Introduce a delay
sharedVariable = currentValue + 1;
mtx.unlock(); // Unlock the mutex
}
}
int main() {
std::thread thread1(increment);
std::thread thread2(increment);
thread1.join();
thread2.join();
std::cout << "Expected value: 200000" << std::endl;
std::cout << "Actual value: " << sharedVariable << std::endl;
return 0;
}
The output:
root@DemoYuChengKuo:~/3_multithreading_cpp/code_02_synchronization# ./7_raceCondition_02
Expected value: 200000
Actual value: 200000
(2) Synchronization Approaches: Mutex, Atomic Operation, & Condition Variable
We’ll see use cases with 3 types of synchronization approaches: Mutex, Atomic Operation, & Condition Variables.
- [1] Mutex: Mutexes are used to protect shared resources by ensuring that only one thread can access the resource at a time.
- [2] Atomic operation: Atomic operations are a type of synchronization that can be used to avoid race conditions without using locks. They ensure that operations on a variable are completed without interruption.
Let’s view an instance with Mutex & Atomic Operation.
// 8_raceCondition_03.cpp
#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>
#include <atomic>
// (1) Race Condition 01: The race condition may not happen if the CPU is faster enough
int sharedVariable_race_01 = 0;
void increment_race_01() {
for (int i = 0; i < 100000; ++i) {
sharedVariable_race_01++;
}
}
// (2) Race Condition 02: Introduce delay to make race condition happen definitely
int sharedVariable_race_02 = 0;
int currentValue_02 = sharedVariable_race_02;
void increment_race_02() {
for (int i = 0; i < 100000; ++i) {
int currentValue_02 = sharedVariable_race_02;
std::this_thread::sleep_for(std::chrono::microseconds(1)); // Introduce a delay
sharedVariable_race_02 = currentValue_02 + 1;
}
}
int sharedVariable_mutex = 0;
std::mutex mtx;
void increment_mutex() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lock(mtx); // Automatically locks and unlocks
sharedVariable_mutex++;
}
}
/*
std::lock_guard is a safer alternative to manual locking and unlocking because it
automatically unlocks the mutex when it goes out of scope.
*/
std::atomic<int> sharedVariable_atomic(0);
void increment_atomic() {
for (int i = 0; i < 100000; ++i) {
sharedVariable_atomic++;
}
}
/*
In this example, sharedVariable is declared as std::atomic<int>.
Atomic operations are indivisible, ensuring that the variable is incremented
correctly even when multiple threads access it concurrently.
*/
int main() {
std::thread thread1(increment_race_01);
std::thread thread2(increment_race_01);
std::thread thread3(increment_race_02);
std::thread thread4(increment_race_02);
std::thread thread5(increment_mutex);
std::thread thread6(increment_mutex);
std::thread thread7(increment_atomic);
std::thread thread8(increment_atomic);
thread1.join();
thread2.join();
thread3.join();
thread4.join();
thread5.join();
thread6.join();
thread7.join();
thread8.join();
std::cout << "Expected value: 200000" << std::endl;
std::cout << "Actual value of Race Condition: " << sharedVariable_race_01 << std::endl;
std::cout << "Actual value of Race Condition: " << sharedVariable_race_02 << std::endl;
std::cout << "Actual value of Mutex: " << sharedVariable_mutex << std::endl;
std::cout << "Actual value of Atomic: " << sharedVariable_atomic.load() << std::endl;
return 0;
}
The output:
The output on my laptop (with Intel i7–1165G7) & my Ubuntu VPS (with AMD EPYC-Rome) are different. Still, we can see the usages of Mutexes & Atomic Operations.
Now, let’s see a more complex application of Mutex: Condition Variable
- [3] Condition Variable: Condition variables are used for complex synchronization problems, particularly when a certain condition needs to be met before a thread can proceed. They are often used in conjunction with mutexes.
// 9_conditionVariable.cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void workerThread() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; });
// Perform work after the condition is met
std::cout << "Worker thread is processing data." << std::endl;
}
int main() {
std::thread worker(workerThread);
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one();
worker.join();
std::cout << "Back in main." << std::endl;
return 0;
}
The output:
root@DemoYuChengKuo:~/3_multithreading_cpp/code_02_synchronization# ./9_conditionVariable
Worker thread is processing data.
Back in main.
(3) References
[1] cloudxlab | Race Condition and Deadlock
[2] Yu-Cheng Kuo | Hands-On Multithreading with C++ 01 — Overview
[3] Yu-Cheng Kuo | OS Walkthrough 02 — Synchronization
[4] ChatGPT-4