Skip to main content

Getting Stuck: When Threads Betray You

 

Getting Stuck: When Threads Betray You

Welcome back to Getting Stuck. This time, instead of the abstract world of Turing machines, I wandered into something that should’ve been simple: writing a little multithreaded program to speed up a task.

It was supposed to be clean, efficient, and elegant. Instead, I ended up chasing ghosts — bugs that showed up, disappeared, and then reappeared when I least expected.


Step 1: The Simple Idea

The task: count how many times a number appears in a giant list.

I thought, Why not parallelize it? With Python’s threading module, I could split the list into chunks and count in parallel.

import threading

numbers = [i % 100 for i in range(10**6)]
count = 0

def worker(chunk):
    global count
    local_count = chunk.count(42)
    count += local_count

threads = []
chunk_size = len(numbers) // 4
for i in range(4):
    t = threading.Thread(target=worker, args=(numbers[i*chunk_size:(i+1)*chunk_size],))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print("Count of 42:", count)

Seems harmless, right?


Step 2: The Bug That Comes and Goes

I ran the program. Sometimes I got the right answer. Other times, the result was… too small.

For example:

Count of 42: 10000

And then:

Count of 42: 9865

Same program. Same input. Different outputs.

This was the first time I felt that familiar Getting Stuck pit in my stomach.


Step 3: Wild Guesses

At first, I blamed Python. Maybe the interpreter was glitching? Maybe the threads weren’t really running in parallel?

So I rewrote it in C with pthread. The same bug appeared. Random wrong answers.

That ruled out Python weirdness.

Then I suspected slicing. Maybe I was cutting the array wrong? I added print statements to show the chunks. Everything was fine.

False leads everywhere.


Step 4: The Real Culprit — Race Conditions

It finally dawned on me. All four threads were doing:

count += local_count

But this isn’t atomic. Under the hood, it’s like:

  1. Read count.

  2. Add local_count.

  3. Write result back.

Two threads doing this at the same time could both read the old value before either one writes back. One update clobbers the other.

That’s why sometimes results were right, sometimes wrong. The order of execution wasn’t guaranteed.


Step 5: The Fix

The fix was to use a lock to protect updates to the shared variable:

lock = threading.Lock()

def worker(chunk):
    global count
    local_count = chunk.count(42)
    with lock:
        count += local_count

Now every update to count was serialized. No clobbering. The result was consistent every time.


Step 6: The Deeper Problem

Happy ending? Not really.

I had solved the immediate race condition, but I noticed something else: performance hadn’t improved at all. In fact, it was slower than the single-threaded version.

Why?

  • The Global Interpreter Lock (GIL) in CPython meant my threads weren’t really parallelizing CPU-bound work.

  • My lock added even more overhead.

If I wanted true parallelism, I needed multiprocessing, not threading. My grand “speedup” had turned into a slowdown.


Reflection

This journey was brutal, because the bug wasn’t obvious.

  • At first, the program worked sometimes — the cruelest kind of bug.

  • I chased wrong explanations (Python quirks, slicing mistakes).

  • The real problem was a race condition, one of the trickiest beasts in CS.

  • Even after fixing it, I realized my whole approach (using threads in Python) wasn’t suited for the problem.

So I got stuck twice: once on the bug, and again on the bigger architectural mistake.

And maybe that’s the truest “getting stuck” there is — when fixing the bug only reveals that your whole plan needs to change.



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...