top of page
Search

Threads Made Me Slower: What Python taught me about concurrency

  • Writer: Andy Brave
    Andy Brave
  • Jul 8
  • 4 min read

Updated: Jul 9

ree

A Java Engineer Enters the Python World

I started my software engineering journey in the world of Java and Unix systems. From the very beginning, I was taught a foundational truth: threads are light, fast, and cheap; processes are heavy, isolated, and expensive. In university, and later in industry, the advice was clear: “If you can avoid creating new processes, do it. Use threads. They’re designed for concurrency.”

Naturally, when I transitioned into Python for some side projects, I carried this same mental model with me. I reached for threading.Thread() the moment I needed to run multiple tasks in parallel. After all, threads had always been the tool for the job. Why would it be any different?


The Surprise: Threads in Python Weren’t Faster

The first time I tried to optimize a CPU-bound task in Python using threads, I was genuinely confused. My expectation was simple: more threads → more speed. So I spun up a handful of threads, each performing some heavy computation — something that would’ve scaled smoothly in Java. But the results were disappointing. CPU usage barely budged, and the program wasn’t any faster. In fact, it felt slower.

I assumed I had made a mistake, so I dove into debugging. I reviewed my logic, checked the loops, restructured the function calls. I even profiled the code, thinking maybe I had misidentified the hotspot. I added timers, logs, tried different data structures, and reran tests with different thread counts. I spent hours chasing a ghost — convinced the bug was in my code.

But the problem wasn’t my logic. The problem was Python itself.That’s when I learned about the Global Interpreter Lock (GIL) — and everything clicked.


Understanding the GIL: Python’s Hidden Limiter

The GIL is a mutex in CPython (the standard Python interpreter) that allows only one thread to execute Python bytecode at a time, even on multi-core machines. This means that if you’re running CPU-bound tasks using threads, they can’t actually run in true parallel. They take turns executing.

Why does the GIL exist? Mostly for simplicity and safety. It protects memory management in CPython’s non-thread-safe runtime. Without it, developers would need to manually manage far more synchronization and deal with subtle bugs.

So while Python threads are fine for I/O-bound tasks (where the thread can wait on disk or network and release the GIL), they’re useless for CPU-bound work that needs real parallelism. In Python, threads are concurrent, but not parallel — and that’s a massive distinction.

In essence, my Java mindset didn’t transfer directly to Python. I had to unlearn the rule “threads are always better” and replace it with something more Pythonic: “Use threads for I/O, use processes for CPU.


When to Use Threads in Python

Despite the GIL, Python threads are still incredibly useful — but only for I/O-bound tasks. These are operations where the program spends most of its time waiting on input/output: like file access, downloading data from the internet, or querying a database.

In these cases, the thread releases the GIL while it waits, allowing other threads to run. So while it’s not parallel, it’s still concurrent, and can significantly speed up programs that would otherwise sit idle during I/O waits.


🔧 Common I/O-bound tasks:

  • Web scraping

  • File downloads/uploads

  • Reading large files

  • Database queries

  • Network services (sockets, APIs)


When to Use Multiprocessing in Python

For CPU-bound tasks — anything involving heavy computation — multiprocessing is the tool of choice. Unlike threads, each process runs in its own interpreter with its own GIL, which means they can run in true parallel on separate CPU cores.

🧠 Why it works:

  • Python’s GIL only limits threads, not processes

  • Each process has its own memory space and Python interpreter

  • Great for tasks that max out the CPU


🔧 Common CPU-bound tasks:

  • Image and video processing

  • Data analysis, ML model training

  • Math-heavy algorithms

  • File compression or encryption

  • Parsing and transformations at scale


Threads vs Multiprocessing: Quick Comparison Table

Feature

threading

multiprocessing

Best for

I/O-bound tasks

CPU-bound tasks

Uses shared memory?

✅ Yes

❌ No (separate memory spaces)

GIL limited?

❌ Yes (threads compete for GIL)

✅ No (each process has its own GIL)

True parallelism?

❌ No

✅ Yes

Setup cost

Low (lightweight)

Higher (processes are heavier)

Communication between workers

Easy (shared variables)

Harder (use Queue, Pipe, etc.)

Overhead

Minimal

Higher, due to separate processes

Debugging

Easier

Trickier (especially across platforms)

My Takeaway as an Engineer

Coming from Java and Unix, I assumed the concurrency rules I’d learned were universal: threads are always better than processes — faster, cheaper, simpler.

But Python taught me otherwise.

Understanding the GIL shifted my mental model. I had to stop thinking in terms of “just spawn a few threads” and start thinking about the nature of the task itself. Is it CPU-heavy? Use processes. Is it I/O-heavy? Use threads. This distinction became not just a performance tweak, but a fundamental design decision.

In Python, you can't assume concurrency == speed. You have to understand:

  • What is the bottleneck?

  • How does the GIL affect your code?

  • Which parts of your app are waiting vs working?

That’s what makes Python concurrency special: it forces you to know your workload deeply before choosing your tools.


Conclusion

If you’re new to Python and concurrency, here’s the hard truth:

Don’t blindly use threads expecting performance gains.

Python is different — not worse, not better, just unique. The GIL makes threading useful only in specific situations, and real parallelism only comes with multiprocessing.

So before you build that next parallel task, ask:

  • Is this I/O-bound? → Use threading or asyncio

  • Is this CPU-bound? → Use multiprocessing

Knowing that difference will save you time, frustration, and a lot of head-scratching.

In the end, concurrency in Python isn’t just about speed — it’s about strategy. And once you learn the rules, you can bend them to your will.


 
 
 

Comments


Post: Blog2_Post

©2022 by andybravo.

bottom of page