I’m hoping that you went through the conceptual understanding of concurrency in one of the previous blog Concurrency and Parallelism https://medium.com/@pradip.dharam/concurrency-and-parallelism-18a41ddb6547. If not visited, please do.
While executing code having to launch many threads. Code needs to be thread safe. Race Condition, Memory Visibility and Instructions Reordering.
1. Race Condition
Below operations on shared variable leads to race condition. You usually do not detect race condition when iteration happening on shared variable are less. You need to rerun the code multiple times to detect race condition. Program becomes unsafe to the thread. We need to make the code thread safe.
- Fetch
- Compute
- Write
Program itself is illustrative
"""I do not see race condition at all
You're absolutely right to be skeptical—race conditions in Python threads using
CPython may not manifest easily due to the Global Interpreter Lock (GIL).
Even though Python threads can interleave operations in a way that theoretically
causes a race condition, the GIL often serializes thread execution just enough
that it appears safe in simple CPU-bound operations like counter += 1.
"""
import threading
import time
# Shared variable
counter = 0
def increment():
global counter
for _ in range(100000):
counter += 1 # This operation is not atomic!
# Create two threads that run the increment function
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=increment)
# Start the threads
thread1.start()
thread2.start()
# Wait for both threads to complete
thread1.join()
thread2.join()
# Expected: 200000 (100000 + 100000), Actual: often less due to race condition
print("Final counter value:", counter)
"""But: the race condition is still real, just harder to observe without
forcing it to manifest. Let me help you see it more clearly.
🔁 Force the Race Condition to Appear
We can do two things:
Add a tiny sleep between reading and writing the shared variable.
Split the += into explicit load/modify/store steps.
Here's a modified version that forces the race condition:
### Output:
This gives me output close to 100000 and not 500000
Each thread is:
Reading the current value of counter
Sleeping for 50 microseconds — letting another thread run and
possibly update the counter
Writing back its stale version + 1, overwriting whatever happened
during the sleep
This results in most of the increments being lost, and only a
small fraction (e.g., ~100,000 out of 500,000) actually being committed.
"""
counter = 0
def increment():
global counter
for _ in range(100000):
current = counter # Read
time.sleep(0.00001) # Force thread switch
counter = current + 1 # Write
# Start two threads
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=increment)
thread3 = threading.Thread(target=increment)
thread4 = threading.Thread(target=increment)
thread5 = threading.Thread(target=increment)
thread1.start()
thread2.start()
thread3.start()
thread4.start()
thread5.start()
thread1.join()
thread2.join()
thread3.join()
thread4.join()
thread5.join()
print("Final counter value:", counter)
"""
How to Fix above program
Use a Lock to make the critical section (read-modify-write) atomic:
This ensures that only one thread can execute the critical section at a time,
"""
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
with lock:
current = counter
time.sleep(0.00001)
counter = current + 1
threads = []
for _ in range(5):
t = threading.Thread(target=increment)
t.start()
threads.append(t)
for t in threads:
t.join()
print("Final counter value with lock:", counter)
# Final counter value with lock: 500000
"""
FINAL OUTPUT
(.env-312) pradip@machine:~/workspace/concurrency$
cd /home/pradip/workspace/concurrency ; /usr/bin/env /home/pradip/
workspace/concurrency/.env-312/bin/python /home/pradip/.vscode/
extensions/ms-python.debugpy-2025.10.0-linux-x64/bundled/libs/
debugpy/adapter/../../debugpy/launcher 51783 -- /home/pradip/
workspace/concurrency/3_race_condition.py
Final counter value: 200000
Final counter value: 100001
Final counter value with lock: 500000
"""
# Disclaimer: This code was generated with assistance from
# Chatgpt, an AI-powered assistantCritical section of a code section which can indulge into race condition.
Making you code thread safe means many things. One of the thing it means is to guard your critical sections, to make sure that critical section of the code does not indulge into race condition.
Number of iterations executed by a shared section or critical code section are less, in that case, you get lucky and never ever see that the threads executing the critical section encountered the race condition scenario. If number of iterations executed by a shared section is too high, then high chance that the threads encounter a race condition.
Main reason race condition occurs is that; one thread has fetch-compute-update, if this thread id not completely done with its fetch-compute-update; another thread starts and it fetches the stale value of the variable. CPU scheduler needs to avoid this scenario. Do not execute another thread in between if one thread is in between its fetch-compute-update operation. Let each thread perform fetch-compute-update operation completely and do not allow other thread to start in between. Do not allow another thread start its fetch-compute-update operation until current thread completes its own fetch-compute-update operation. Context switches will keep on happening between threads; but no other thread will be allowed to access the critical section if one of the thread is operating on that critical section.
CPU scheduler itself does the context switches. CPU scheduler is not the one to help me protect the critical section of the code the way I want that no other thread should interface upon context switch if one of the thread is executing on critical section. I need to do something to protect the critical section of the code in multi threaded environment to avoid the race condition, CPU scheduler itself is not going to help here.
Locking
Making the code section atomic. Atomic is guarding the code section with locks.
Consider there is only one lock. The lock x. If a thread 1 acquires a “lock x” on a critical section of the code, other thread wont be able to acquire the “lock x” on critical section of the code until thread 1 releases the “lock x” on critical section of the code. In case of only one lock on the critical section, code is going to be guaranteed thread safe, no chance of race condition.
Consider there two locks. Lock x and lock y. If a thread 1 acquires a “lock x” on a critical section of the code, other thread wont be able to acquire the “lock x” on critical section of the code. However, other thread will be able to acquire “lock y” on the critical section, even if the “lock x” is not released from the critical section. As there are 2 locks into picture, and critical section cannot be thread safe, as two locks; lock x and lock y can simultaneously access critical section, it leads to race condition.
Identify the critical section as lowest most layer in the code, and then apply the lock on that lowest most layer of the code. Otherwise, it degrades the performance, increases the overall runtime & enables threads to execute code in almost serial manner.
Compound Actions
Both getter and setter methods should be synchronized using one lock. Compound actions can lead to inconsistent data while reading if not done in thread safe manner.
Thread Safety Summary
Program is thread safe when multi threaded program behaves the same way as of single threaded. Thread safe class is a class which works well for single threaded application, it should work well for muddleheaded application of that class and clients of that class should not worry for synchronization of code where that class is used.
3 ways to ensure whether code is thread safe
- Multiple threads running and have no shared data or variables at all which threads are accessing or modifying. This means that no section of code is critical, hence there is no question of occurring a race condition.
- Multiple threads running and have no shared data. But that shared data is read only (immutable). This means that no section of code is critical, and no chance of engaging into a race condition. Marking variable final (makes the variable read only) in java is ensuring the thread safety, lot of organization follow this as a standard coding practice, in case the data is shared across threads, it won’t lead to race conditions.
- In real world, every time its not possible to ensure that the threads are dealing with immutable data. There may be multiple threads, dealing with text file having strings, those threads are parsing those strings, and writing to the shared list or to a common buffer; in this case race condition can creep in. That list or buffer is shared across multiple threads. Thread synchronization (acquiring the lock on critical section of code) comes for our rescue to deal with race condition.
2. Memory Visibility Problem
10 threads accessing one variable.
Shared variable is created on the main memory (RAM). For faster computation, thread makes one copy of that variable on to memory of CPU. Thread periodically flushes the changes value in the CPU copy to the main memory copy; however, copying of value from CPU memory to main memory is not guaranteed, its non deterministic. So, other threads may not find the updated value in the copy which resides in the main memory. This is called memory visibility problem.
On Java programming, declare that shared variable as volatile using volatile keyword. Variables which are volatile guarantees that the CPU copy gets flush to main memory copy as soon as thread changes the value in the CPU memory copy. As a result, changed value of the shared variable is available immediately to other threads.
Note that memory visibility can happen any time. Even if we see correct output without handling Memory Visibility Problem, we just got lucky and nothing else. Its very common with concurrent program, that code runs for years in a correct way and all off of a sudden one day things can burst, you will realize a pain of debugging an issue.
Engineers these days very rarely use the volatile keyword as performance concerns are associated with it. There are alternate ways to deal with memory visibility problem.
Differentiating between first danger (Race Condition) and second danger (Memory Visibility Problem)
One thread only reads the variable, another thread only writes the same variable. Race condition is not going to happen here. Memory Visibility Issue is going to happen. Synchronized keyword in Java helps here. Synchronized is needed, threads mutating the shared variable leads to race condition.
10 threads, code which ultimately becomes critical where shared variable is read on first line and updated on send line. This leads to Race Condition where threads are going to interfere between each other results in incorrect output. Volatile keyword in Java helps here. Volatile is needed because changes made to the variable by one thread might not be visible to another thread.
Hence, first danger and second dangers are very much different. Both dangers are independent of one another.
Alternate way to take care of Memory Visibility Issue. Synchronized keyword also helps address Memory Visibility Issue. Read first comment in below program very carefully.
""" In Python, the alternative to Java’s synchronized is using
a threading.Lock (or RLock) with a with statement.
This ensures only one thread can execute the code inside the
with lock: block at a time, similar to Java’s synchronized.
Yes, using the same lock for get and set helps prevent memory
visibility issues in Python.
When a thread acquires a lock, changes made to shared data become
visible to other threads when the lock is released.
This ensures all threads see the most up-to-date value."""
import threading
class SafeValue:
def __init__(self, value=0):
self._value = value
self._lock = threading.Lock()
def get(self):
with self._lock:
return self._value
def set(self, value):
with self._lock:
self._value = value
# Usage example
safe = SafeValue(10)
def worker():
for _ in range(1000):
safe.set(safe.get() + 1)
threads = [threading.Thread(target=worker) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
print(safe.get())
# Disclaimer: This code was generated with assistance from GitHub Copilot,
# an AI-powered coding assistant3. Instructions Reordering
"""Instruction reordering is extremely unlikely to occur in the code
you've written because of Python's GIL and memory model — especially
when using CPython, the standard Python interpreter.
In CPython, instruction reordering will not happen for:
self.x += 1
self.y += 1
self.z += 1
self.w += 1
because of:
The GIL preventing concurrent bytecode execution,
Python’s higher-level memory model, and
"""
import threading
class NumberStore:
def __init__(self):
self.x = 0
self.y = 0
self.z = 0
self.w = 0
def increment(self):
self.x += 1
self.y += 1
self.z += 1
self.w += 1
def get_x(self):
return self.x
def get_y(self):
return self.y
def get_z(self):
return self.z
def get_w(self):
return self.w
number_store = NumberStore()
# Running the thread multiple times
for _ in range(1000000):
t = threading.Thread(target=number_store.increment)
current = number_store.get_w()
t.start()
# while current == number_store.get_w():
# pass
# Inlined content of check_order()
if not (number_store.get_w() == number_store.get_z() or
number_store.get_z() == number_store.get_y() or
number_store.get_y() == number_store.get_x()):
print(f"Reordering detected: \
x={number_store.get_x()}, \
y={number_store.get_y}, \
z={number_store.get_z}, \
w={number_store.get_w}")
t.join()
# If your Python program were translated into Java and run under similar
# conditions, you could observe memory reordering issues, output below.
public class NumberStore {
int x = 0;
int y = 0;
int z = 0;
int w = 0;
public void increment() {
x += 1;
y += 1;
z += 1;
w += 1;
}
public int getX() { return x; }
public int getY() { return y; }
public int getZ() { return z; }
public int getW() { return w; }
public static void main(String[] args) throws Exception {
NumberStore store = new NumberStore();
for (int i = 0; i < 1000000; i++) {
Thread t = new Thread(() -> store.increment());
int currentW = store.getW();
t.start();
while (store.getW() == currentW) {
// busy-wait until increment happens
}
int x = store.getX();
int y = store.getY();
int z = store.getZ();
int w = store.getW();
if (w > z || z > y || y > x) {
System.out.printf("Reordering detected: x=%d, y=%d, z=%d, w=%d%n", x, y, z, w);
}
t.join();
}
}
}
Output:
Reordering detected: x=1, y=1, z=1, w=0
Reordering detected: x=2, y=2, z=1, w=1
Reordering detected: x=4, y=3, z=3, w=3
# Solution to the reordering is synchronized keyword
# Modified increment function as below
public void increment() {
synchronized {
x += 1;
y += 1;
z += 1;
w += 1;
}
}
# Disclaimer: This code was generated with assistance from GitHub Copilot
# and chatgpt, an AI-powered coding assistantFeel free to reach out on LinkedIn for networking.