The Secret Life of Python: Fixing Race Conditions with Threading Locks
The Secret Life of Python: Fixing Race Conditions with Threading Locks
Why x += 1 breaks in threads and how to protect shared data
#Python #Threading #Concurrency #SoftwareEngineering
Margaret is a senior software engineer. Timothy is her junior colleague. They work in a grand Victorian library in London — the kind of place where code quality is the unspoken objective, and craftsmanship is the only thing that matters.
Episode 32
Timothy was ready to give his Chess Club app its second pair of hands. Following Margaret's advice, he opened the threading module.
"I’ve set up two threads," Timothy explained, typing furiously. "One for the 'Grandmaster Match' and one for the 'Junior Match.' They both update a single global variable called total_moves_recorded. Since they’re running at the same time, the club's live dashboard should update twice as fast!"
He wrote a simple loop where each match recorded 100,000 moves. If everything worked, the final count should be exactly 200,000.
import threading
total_moves_recorded = 0
def record_match_moves():
global total_moves_recorded
for _ in range(100000):
total_moves_recorded += 1
# Create two 'workers' (threads)
thread_a = threading.Thread(target=record_match_moves, name="Grandmaster")
thread_b = threading.Thread(target=record_match_moves, name="Junior")
thread_a.start()
thread_b.start()
thread_a.join() # Wait for A to finish
thread_b.join() # Wait for B to finish
print(f"Final Count: {total_moves_recorded}")
Timothy hit enter. The screen blinked: Final Count: 142634. He ran it again: Final Count: 157201.
"Margaret!" Timothy shouted. "My code is losing moves! It's like the workers are stealing from the counter!"
The Interruption
Margaret leaned over. "They aren't stealing, Timothy. They’re just interrupting each other. You’ve just met the reality of the Read-Modify-Write cycle."
She drew a diagram of the total_moves_recorded += 1 line.
"To you, that’s one step," she said. "But to Python, it’s three:
- Read the current value (e.g., 10).
- Modify it (10 + 1 = 11).
- Write the new value back (11).
"The problem," Margaret continued, "is that the Grandmaster thread might Read 10, but before it can Write 11, the Operating System pauses it and lets the Junior thread take over. The Junior thread also Reads 10 and eventually Writes 11. Then the Grandmaster wakes up and finishes its job by Writing 11 again. Two moves happened, but the counter only went up once."
The Specialist’s Key: The Lock
"So they’re overwriting each other's work," Timothy whispered. "How do I make them wait their turn?"
"You need a Lock," Margaret said. "Think of it as a 'Talking Stick' for your threads. Only the thread holding the stick is allowed to touch the variable. Everyone else has to wait in the hallway."
She showed him the fix:
move_lock = threading.Lock()
def record_match_moves():
global total_moves_recorded
for _ in range(100000):
with move_lock: # The Safe Room returns!
total_moves_recorded += 1
Timothy ran the code. Final Count: 200000. He ran it ten more times. Every single time, the result was perfect.
"The with statement!" Timothy realized. "It's the Safe Room again!"
The Cost of Safety
"Exactly," Margaret nodded. "By using with move_lock, you ensure that the Read-Modify-Write cycle is never interrupted. But remember: while a thread is inside that 'Safe Room,' the other thread is standing still. If you lock too much of your code, you're back to being Synchronous—and slow."
Timothy looked at his perfect 200,000. He had stopped the race, but he realized that being a Specialist meant balancing Safety with Speed.
Margaret’s Cheat Sheet: The First Lock
- Read-Modify-Write: Simple lines like
x += 1are not "atomic." They can be interrupted halfway through, leading to data loss. threading.Lock: Use this to synchronize threads. It acts as a "Talking Stick" to ensure only one worker touches shared data at a time.- The "Safe Room" Connection: Using
with lock:is a Context Manager. It automatically "Locks" the door when you enter and "Unlocks" it when you leave—even if the code crashes. - Specialist Tip: If you can't use a
withblock, always use atry/finallyblock to ensure the lock is released:lock.acquire()followed byfinally: lock.release(). - Lock Contention: Only lock the specific line that touches the shared data. If threads spend too much time waiting for a Lock, your performance will tank.
Aaron Rose is a software engineer and technology writer at tech-reader.blog.
Catch up on the latest explainer videos, podcasts, and industry discussions below.
.jpeg)

Comments
Post a Comment