Skip to main content

Getting Stuck: The Deadlock That Ate My Program

 

Getting Stuck: The Deadlock That Ate My Program

In this installment of Getting Stuck, I set out to do something totally ordinary: write a tiny banking system where two accounts can transfer money to each other. Easy enough, right? Just some locks and arithmetic.

Instead, I ended up with a program that sometimes worked, sometimes froze solid, and once even locked itself in an eternal staring contest with my CPU.

This is the story of my brush with deadlock.


Step 1: The Setup

I wanted a simple class for accounts:

import threading

class Account:
    def __init__(self, balance):
        self.balance = balance
        self.lock = threading.Lock()

    def withdraw(self, amount):
        self.balance -= amount

    def deposit(self, amount):
        self.balance += amount

Then a function to transfer between accounts:

def transfer(src, dst, amount):
    src.lock.acquire()
    dst.lock.acquire()

    src.withdraw(amount)
    dst.deposit(amount)

    src.lock.release()
    dst.lock.release()

Seems straightforward. Two locks, a withdraw, a deposit, and we’re done.


Step 2: The Hang

I spun up a quick test:

a = Account(100)
b = Account(100)

threads = []
for _ in range(100):
    t1 = threading.Thread(target=transfer, args=(a, b, 1))
    t2 = threading.Thread(target=transfer, args=(b, a, 1))
    threads.extend([t1, t2])

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

Sometimes it worked. Other times, the program froze. No error, no crash — just silence.

I stared at it for minutes, convinced I’d missed a typo. But everything looked fine.


Step 3: False Leads

I suspected the balance arithmetic. Maybe withdraw was letting balances go negative? I added print statements. Balances were fine.

Then I thought maybe I was exhausting threads. I cut the loop down to 2 iterations. Still froze.

I even wondered if Python’s GIL was messing with me. But I knew enough to realize: the GIL prevents true parallelism, but it doesn’t cause this.

It was something else.


Step 4: Seeing the Pattern

After running the program dozens of times, I noticed a pattern: it only froze when one thread tried transfer(a, b, 1) while another tried transfer(b, a, 1) at the same time.

That was my “aha” moment.

Thread 1 grabbed a.lock.
Thread 2 grabbed b.lock.
Thread 1 waited for b.lock.
Thread 2 waited for a.lock.

Both threads were waiting for each other, forever.

I had built a textbook deadlock.


Step 5: The Fix

The classic fix is to impose an ordering on lock acquisition. Always grab locks in the same global order, no matter what.

def transfer(src, dst, amount):
    first, second = sorted([src, dst], key=lambda x: id(x))
    first.lock.acquire()
    second.lock.acquire()

    src.withdraw(amount)
    dst.deposit(amount)

    second.lock.release()
    first.lock.release()

Now no two threads can get into a circular wait. Deadlock avoided.


Step 6: The Bigger Lesson

The fix worked — but what struck me was how invisible the bug was. There was no error message, no stack trace. Just… nothing.

Deadlocks are scary because they’re not “fail fast.” They’re fail silently. They hide until the exact unlucky interleaving of events triggers them, and then they freeze everything.

And that makes them the ultimate “getting stuck.” Not only was my program stuck, but so was I, staring at it, unsure where to even begin looking.


Reflection

This episode of Getting Stuck left me with a deeper respect for concurrency bugs.

  • A single wrong order of locks can doom an entire system.

  • Debugging deadlocks is like looking for ghosts.

  • The solution often isn’t fancy — just a simple rule, like ordering lock acquisition.

But the real takeaway is humility. Because if this tiny toy program can deadlock, what about massive systems with hundreds of threads, locks, and resources?

Getting stuck on a bug like this teaches you that sometimes the hardest problems aren’t about code that doesn’t run — they’re about code that never stops running.



Comments

Popular posts from this blog

Sharp Extremal Bounds for Angular Occupancy (Behind the Curtains)

you can find the full paper here First Attempts, and the Limits of Energy When I first began thinking about angular occupancy, I wasn’t looking for a brand-new problem. I was chasing a theme that has long fascinated me in discrete geometry: how do we measure the geometric richness of a finite set of points? This fascination is not unique to me. Erdős, way back in 1946, posed the distinct distances problem : given n points in the plane, how many distinct distances can they determine? That question alone gave birth to entire decades of work in combinatorial geometry. Later, Fishburn and Füredi asked about distinct angles , while Pach and Sharir studied repeated angles . Each time, the same underlying itch was being scratched: when you place points in the plane, what kinds of diversity can they generate? Why angles, why bins? It was natural to think of angles — after all, distances and directions had already been well studied. But if you go straight for “distinct angles,” you run in...

The Art of Goodbye

  The Art of Goodbye: How Relationships End and Why It Matters The friendship had been slowly dissolving for months. What used to be weekly coffee dates became monthly check-ins, then sporadic text messages, then silence. No fight precipitated the ending, no dramatic confrontation or betrayal. It simply... faded. One day you realized you hadn't spoken to someone who was once central to your life, and you weren't sure when the relationship had officially ended or even if it had. This ambiguous loss left you with a peculiar grief – mourning someone still alive, still accessible, but no longer present in your world. This experience is incredibly common yet rarely discussed. While we have cultural scripts for how relationships begin – meet-cutes, first dates, friendship origin stories – we have few models for how they end. We talk extensively about building connections but rarely about gracefully releasing them. This gap in our social understanding leaves many people unprepared ...

Getting Stuck: When Dijkstra Meets Negative Edges

  Getting Stuck: When Dijkstra Meets Negative Edges One of the joys (and pains) of computer science is when you take a tool you know, confidently apply it, and then watch everything break down in surprising ways. This happened to me recently when I decided to revisit shortest path algorithms — specifically, Dijkstra’s algorithm — only to get stuck in a swamp of negative edges. Setting the Scene The problem was classic: Given a directed, weighted graph and a source vertex, find the shortest path to all other vertices. This is the kind of problem that every CS student learns early, usually with Dijkstra’s algorithm. I already knew Dijkstra like an old friend: Keep a priority queue of distances. Always expand the node with the current smallest distance. Relax its edges. Simple, elegant, efficient ( O((V + E) log V) with a binary heap). So when I started coding a graph utility for my project — a tool to analyze transportation networks — I confidently plugged in D...