QA - Section 1.04. Synchronization & Shared Resources
QA - Section 1.04. Synchronization & Shared Resources
1. What is a Race Condition?
A race condition occurs when the outcome of a program depends on the timing or order of execution between multiple threads. In other words, the result becomes unpredictable because several threads access shared state without proper coordination.
Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <thread>
#include <iostream>
int counter = 0;
void increment()
{
for (int i = 0; i < 100000; ++i)
++counter;
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << counter << '\n'; // expected 200000, but may be smaller
}
Here, both threads update counter at the same time, so the final result is not reliable.
2. What is the Difference Between a Data Race and a Race Condition?
A data race is a specific low-level issue where two or more threads access the same memory location concurrently, at least one access is a write, and there is no synchronization. This leads to undefined behavior in C++.
A race condition is a broader concept. It means the correctness of the program depends on timing or ordering. Not every race condition is a data race, but every data race is a race condition.
Example of Data Race
1
2
3
4
5
6
7
8
9
10
int x = 0;
void writer()
{
x = 42;
}
void reader() {
std::cout << x << '\n';
}
If writer and reader run concurrently without synchronization, this is a data race.
3. Mutex vs Spinlock
A mutex puts a thread to sleep if the lock is unavailable, while a spinlock repeatedly checks the lock in a loop until it becomes available.
Mutex
- Better for longer critical sections
- Avoids wasting CPU while waiting
Spinlock
- Better for very short critical sections
- Can be faster when waiting time is extremely small
- Wastes CPU if held for too long
Mutex Example
1
2
3
4
5
6
7
8
9
10
#include <mutex>
std::mutex m;
int counter = 0;
void work()
{
std::lock_guard<std::mutex> lock(m);
++counter;
}
Simple Spinlock Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <atomic>
class SpinLock
{
std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
void lock()
{
while (flag.test_and_set(std::memory_order_acquire))
{
// busy wait
}
}
void unlock()
{
flag.clear(std::memory_order_release);
}
};
SpinLock spin;
int counter = 0;
void work()
{
spin.lock();
++counter;
spin.unlock();
}
4. What Causes Deadlock, and How Can You Prevent It?
Deadlock happens when threads wait forever for each other’s locks.
Conditions for Deadlock
- Mutual exclusion
- Hold and wait
- No preemption
- Circular wait
If all four conditions exist, deadlock can happen.
Deadlock Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <mutex>
#include <thread>
std::mutex m1, m2;
void task1()
{
std::lock_guard<std::mutex> lock1(m1);
std::lock_guard<std::mutex> lock2(m2);
}
void task2()
{
std::lock_guard<std::mutex> lock1(m2);
std::lock_guard<std::mutex> lock2(m1);
}
If task1 locks m1 and task2 locks m2 first, both may wait forever.
Prevention
- Always lock in the same order
- Use
std::scoped_lock - Reduce lock scope
- Avoid nested locks when possible
Safer Example
1
2
3
4
5
6
7
8
9
#include <mutex>
std::mutex m1, m2;
void safe_task()
{
std::scoped_lock lock(m1, m2);
// critical section
}
5. Livelock vs Starvation
A livelock happens when threads keep reacting to each other and changing state, but no thread makes progress.
A starvation happens when a thread never gets the resources it needs because other threads keep getting priority.
- Livelock: threads are active but make no progress
- Starvation: one thread is blocked from progressing for a very long time
Livelock Example Idea
Two threads repeatedly release and retry a lock to avoid deadlock, but both keep doing that forever.
Starvation Example Idea
A low-priority thread keeps waiting because high-priority threads always run first.
Deadlock vs Livelock vs Starvation
| Feature | Deadlock | Livelock | Starvation |
|---|---|---|---|
| Definition | Threads wait forever for each other | Threads keep reacting but make no progress | A thread never gets a chance to run |
| State | Blocked (stopped) | Active (running) | One thread blocked or delayed |
| CPU Usage | Low (threads not running) | High (threads keep running) | Varies (some run, some don’t) |
| Progress | ❌ No progress | ❌ No progress | ❌ One thread makes no progress |
| Cause | Circular waiting for locks | Overreaction / retry logic | Unfair scheduling / priority |
| Example Scenario | A waits for B, B waits for A | Threads keep retrying and releasing locks | Low-priority thread never runs |
| Behavior | Completely stuck | Keeps changing state | One thread is ignored |
| Fix | Lock ordering, std::lock | Backoff / randomness | Fair scheduling, priority control |
- Deadlock: threads are blocked waiting for each other
- Livelock: threads are active but useless
- Starvation: one thread never gets resources
6. Atomic vs Mutex
An atomic is used for simple operations on a single variable without locking. A mutex protects larger critical sections or multiple related variables.
Atomic
- Fast for simple counters, flags, state variables
- Limited to small, isolated operations
Mutex
- Works for complex shared state
- Higher overhead, but more flexible
Atomic Example
1
2
3
4
5
6
7
8
#include <atomic>
std::atomic<int> counter = 0;
void increment()
{
++counter;
}
Mutex Example
1
2
3
4
5
6
7
8
9
10
11
#include <mutex>
std::mutex m;
int x = 0, y = 0;
void update()
{
std::lock_guard<std::mutex> lock(m);
++x;
++y;
}
If multiple variables must stay consistent together, mutex is usually the better choice.
7. When Do You Use std::condition_variable?
A condition variable is used when one thread must wait until a specific condition becomes true, instead of continuously polling.
Typical use cases:
- Producer-consumer queue
- Task scheduling
- Waiting for data availability
Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <condition_variable>
#include <mutex>
#include <queue>
#include <thread>
#include <iostream>
std::mutex m;
std::condition_variable cv;
std::queue<int> q;
bool done = false;
void producer()
{
{
std::lock_guard<std::mutex> lock(m);
q.push(42);
}
cv.notify_one();
}
void consumer()
{
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, [] { return !q.empty() || done; });
if (!q.empty())
{
std::cout << q.front() << '\n';
q.pop();
}
}
This avoids wasting CPU in a busy-wait loop.
8. Lock-free vs Wait-free
Both are non-blocking concepts, but wait-free is stronger.
Lock-free
- At least one thread always makes progress
- Some individual threads may still starve
Wait-free
- Every thread completes its operation in a bounded number of steps
Intuition
- Lock-free guarantees system-wide progress
- Wait-free guarantees per-thread progress
In practice, wait-free algorithms are much harder to design and are less common.
In practice, lock-free means designing code to avoid locks for better performance, even if some threads may need to retry and take longer to finish. It guarantees that the system as a whole keeps making progress, but not every individual thread. This is commonly used in high-performance systems because it reduces blocking and improves throughput.
On the other hand, wait-free means that every thread is guaranteed to complete its operation in a fixed number of steps, with no retries or delays caused by other threads. While this provides the strongest guarantee, it is very difficult to design and maintain, so it is rarely used in real-world applications.
9. How Do You Reduce Shared Resources?
Reducing shared resources is one of the best ways to improve scalability.
Common methods:
- Use thread-local storage
- Partition data per thread
- Minimize global state
- Use immutable data when possible
- Batch updates instead of frequent shared writes
Thread-local Example
1
2
3
4
5
6
7
8
9
10
11
12
#include <thread>
#include <vector>
#include <numeric>
#include <iostream>
thread_local int local_count = 0;
void work()
{
for (int i = 0; i < 1000; ++i)
++local_count;
}
Each thread gets its own local_count, so no synchronization is needed. It is difference via atomic, atomic is safety of sharing, but thread_local remove sharing.
Per-thread Partial Sum Example
1
2
3
4
5
6
7
8
9
10
11
12
#include <thread>
#include <vector>
#include <numeric>
#include <iostream>
void partial_sum(const std::vector<int>& v, int start, int end, int& result)
{
result = 0;
for (int i = start; i < end; ++i)
result += v[i];
}
Instead of all threads updating one global sum, each thread computes its own partial result and combines them later.
9. When Is a Reader-Writer Lock Useful?
A reader-writer lock is useful when reads are much more frequent than writes.
- Multiple readers can access the data at the same time
- Writers still need exclusive access
This is beneficial when:
- The shared data is read often
- Writes are relatively rare
- Read operations are long enough to benefit from concurrent access
Example with std::shared_mutex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <shared_mutex>
#include <thread>
#include <iostream>
std::shared_mutex rwlock;
int shared_data = 0;
void reader()
{
std::shared_lock lock(rwlock);
std::cout << shared_data << '\n';
}
void writer()
{
std::unique_lock lock(rwlock);
++shared_data;
}
This allows many readers to proceed concurrently, while writers still get exclusive access.