Threads Made Me Slower: What Python taught me about concurrency
- Andy Brave
- Jul 8
- 4 min read
Updated: Jul 9

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