Skip to Content
Course content

203: Thread Synchronization with Locks

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this exact bug trip up some of the most senior developers I know. You write code that looks mathematically perfect, you run it with a single thread, it works every time. Then you scale it up to handle more concurrency, and suddenly, your data starts "leaking" or numbers don't add up, but only 2% of the time. These are race conditions, and they are a nightmare to debug because they are non-deterministic.

The Mystery of the Disappearing Dollars

Let's look at a typical scenario: a simple bank account balance. In the code below, I'm simulating two different threads trying to deposit money into the same account simultaneously.

import threading

class BankAccount:
    def __init__(self):
        self.balance = 0

    def deposit(self, amount):
        # Pretend there's some slight processing delay here
        current_balance = self.balance
        self.balance = current_balance + amount

account = BankAccount()

def worker():
    for _ in range(100000):
        account.deposit(1)

threads = [threading.Thread(target=worker) for _ in range(2)]

for t in threads: t.start()
for t in threads: t.join()

print(f"Final Balance: {account.balance}")
# Expected: 200000
# Actual: Something like 142384

If you run this, you'll notice the final balance is almost never 200,000. You might wonder why this happens in Python, given that we have the Global Interpreter Lock (GIL). Here is the catch: the GIL prevents multiple threads from executing Python bytecode at the exact same time, but it doesn't stop the OS from switching threads in the middle of a Python operation.

The line self.balance = current_balance + amount isn't one single action. Python has to read the value, add to it, and then write it back. If Thread A reads the balance (say, 100), and then the OS pauses it to let Thread B run, Thread B might also read 100, add 1, and save 101. When Thread A wakes back up, it still thinks the balance is 100, adds 1, and saves 101. We just lost a whole deposit.

Stopping the Race with threading.Lock

To fix this, we need a way to tell Python: "While I am updating this specific variable, nobody else is allowed to touch it." This is where a Lock (or Mutex) comes in. Think of it like a bathroom key at a coffee shop; if you have the key, you're the only one in the room. Everyone else has to wait in line until you bring the key back.

import threading

class BankAccount:
    def __init__(self):
        self.balance = 0
        # We create the lock as part of the object's state
        self.lock = threading.Lock()

    def deposit(self, amount):
        # The 'with' statement automatically acquires and releases the lock
        with self.lock:
            current_balance = self.balance
            self.balance = current_balance + amount

account = BankAccount()

def worker():
    for _ in range(100000):
        account.deposit(1)

threads = [threading.Thread(target=worker) for _ in range(2)]

for t in threads: t.start()
for t in threads: t.join()

print(f"Final Balance: {account.balance}")
# Result: 200000 (Every single time)

By wrapping the balance update in with self.lock:, we ensure that the "read-modify-write" cycle is atomic. If Thread B hits that line while Thread A is still inside the block, Thread B will simply stop and wait until Thread A exits the block. Only then will Thread B acquire the lock and perform its update.

One word of caution: don't put too much code inside a lock. The more time a thread spends holding a lock, the more other threads are sitting idle, which kills the performance gains you wanted from using threads in the first place. Keep your locked sections as small and fast as possible.




📋 Practical Task

Building a Thread-Safe Warehouse Inventory Manager

You are tasked with creating an inventory system for a high-volume warehouse. Multiple "Picker" threads are removing items from the stock, and multiple "Restocker" threads are adding items. Without synchronization, the inventory count will become corrupted.

Requirements:

  • Create a class WarehouseInventory that tracks a dictionary of items (e.g., {"widgets": 100, "gadgets": 100}).
  • Implement a method update_stock(item_name, amount) that adds amount to the item's current count (use negative numbers for removals).
  • Use a threading.Lock to ensure that updates to the inventory are thread-safe.
  • Create 5 "Restocker" threads that each add 1,000 units to "widgets" and 5 "Picker" threads that each remove 1,000 units from "widgets".
  • Verify that the final count for "widgets" returns exactly to its starting value.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.