Concurrency

Master Python's concurrency models and learn when to use threads, processes, or AsyncIO for maximum performance.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

🔥 1. Why Concurrency Exists

Type of Work

What It Means

Best Solution

Real Example

CPU-bound

Heavy calculations that keep the CPU busy

multiprocessing

Image processing, ML training

I/O-bound

Waiting for external resources

threads or asyncio

API calls, file downloads

⚙️ 2. Threads in Python

A thread is a lightweight unit of execution within a single process.

What Threads Share

Why It Matters

Memory

Fast communication, but risk of conflicts

Variables

Easy data sharing, but need locks for safety

File handles

Can work with same files simultaneously

Python interpreter

Limited by GIL for CPU work

🧠 3. Understanding the GIL (Global Interpreter Lock)

The GIL (Global Interpreter Lock) is a mutex that protects access to Python objects.

Situation

GIL Effect

Result

Running Python code

GIL is held

Other threads wait

Waiting for network/file

GIL is released

Other threads can run

Using C extensions (NumPy)

Often released

True parallelism possible

(e.g., image processing, hashing, compression)

⚡ 4. Processes in Python

A process is a full Python interpreter with its own memory.

Aspect

Threads

Processes

Shared

Separate (isolated)

GIL

Shared (limits CPU work)

Each has its own

Startup time

Fast (microseconds)

Slow (milliseconds)

Data sharing

Easy (same memory)

Requires serialization

🔄 5. Side-by-Side Comparison

Feature

Speed for I/O

⭐⭐⭐⭐⭐

⭐⭐⭐

Speed for CPU

⭐⭐

Memory usage

Low

High

Fast

Slow

Shares memory?

Yes

No

Avoids GIL?

Best for?

I/O tasks

CPU tasks

🧪 6. Real-World Examples

Threads shine because requests are I/O-bound.

🧱 7. Mixing Threads & Processes (Hybrid Model)

Component

Role in Hybrid System

Why?

AsyncIO

Orchestra conductor

Lightweight coordination of thousands of tasks

Threads

I/O specialists

Handle blocking I/O without stopping AsyncIO

Processes

Heavy lifters

CPU work on multiple cores simultaneously

This is how production-grade scrapers, ML preprocessors, and automation bots work.

🔥 8. When Should You Use What?

🧠 9. How Python Schedules Threads Internally (Advanced)

Python uses cooperative + preemptive scheduling for threads.

The operating system decides when each thread runs, based on CPU availability.

Inside Python, only ONE thread can execute Python bytecode at once.

⚡ 10. The True Strength of Threads: I/O Parallelism

Let's say you need to download 10,000 images.

Sequential time = 10,000 × (0.4 seconds each) = ~4000 seconds (1.1 hours)

Threaded time (200 threads) = ~20 seconds total

Because threads wait most of the time, so Python overlaps waits.

🔥 11. The True Strength of Processes: CPU Parallelism

If you run these in threads → NO speed improvement .

If you run these in processes → 4× faster on 4 cores, 12× on 12 cores, etc.

🧬 12-18. Advanced Concurrency Topics

The next sections cover professional-level concurrency patterns:

Lock, RLock, Event, Semaphore, Queue — Safe shared memory primitives

multiprocessing.Queue, Pipe, Manager, shared memory arrays

14. ThreadPoolExecutor vs ProcessPoolExecutor

concurrent.futures abstraction for both models

Combining AsyncIO + Threads + Processes like YouTube/Instagram

Race conditions, deadlocks, blocking calls, serialization issues

CPU-heavy: 8× with processes; I/O-heavy: 50× with threads

✔ Threads for I/O | ✔ Processes for CPU | ✔ AsyncIO for massive concurrency

🧨 19. Race Conditions — The Silent Killer

Symptom

What's Happening

Example

Inconsistent results

Different output each run

Counter shows 987,432 instead of 1,000,000

Lost updates

Changes disappear

Two users edit same record, one is lost

Works sometimes

Timing-dependent bugs

Passes tests locally, fails in production

counter += 1 is NOT atomic. It's 3 instructions:

🧱 20. Fixing Race Conditions With Locks

Locks ensure only ONE thread accesses critical code at once.

Lock Method

What It Does

When to Use

lock.acquire()

Grab the lock (waits if taken)

Manual control needed

lock.release()

Release the lock

After acquire()

with lock:

Auto acquire + release

✅ Always prefer this!

But… ❗ Locks introduce blocking, which could slow threads.

🔒 21-32. Expert-Level Concurrency Patterns

When threads freeze forever waiting on each other

What breaks when passing objects to processes

29. Real Architecture: High-Performance Scraper

ProcessPool for CPU, ThreadPool for I/O, AsyncIO for APIs

Python 3.13+ will enable true parallel threads for CPU work

🎓 Final Summary

You're operating at professional backend engineer level now.

📋 Quick Reference — Concurrency

Tool

Best for

threading.Thread

I/O-bound tasks, network calls

multiprocessing.Process

CPU-bound tasks (bypasses GIL)

threading.Lock()

Protect shared state between threads

queue.Queue()

Thread-safe data passing

multiprocessing.Queue()

Process-safe data passing

You now understand the GIL, when to use threads vs processes, and how to safely share data between concurrent workers.

Up next: Parallelism — use concurrent.futures for a clean high-level API over threads and processes.

Practice quiz

What is the difference between concurrency and parallelism?

  • They are the same thing
  • Parallelism is slower than concurrency
  • Concurrency switches between tasks; parallelism does multiple things at the exact same time
  • Concurrency requires multiple CPU cores

Answer: Concurrency switches between tasks; parallelism does multiple things at the exact same time. Concurrency interleaves tasks; parallelism runs them truly simultaneously on multiple cores.

What is the GIL (Global Interpreter Lock)?

  • A mutex that lets only one thread run Python bytecode at a time
  • A networking protocol
  • A way to lock files
  • A garbage collector

Answer: A mutex that lets only one thread run Python bytecode at a time. The GIL is a mutex ensuring only one thread executes Python bytecode at a time.

For CPU-bound work, which approach actually achieves true parallelism in Python?

  • threading
  • asyncio
  • None — Python can't parallelize
  • multiprocessing

Answer: multiprocessing. Each process has its own interpreter and GIL, so multiprocessing gives true CPU parallelism.

Why do threads work well for I/O-bound tasks despite the GIL?

  • The GIL is disabled for threads
  • A thread releases the GIL while waiting on I/O, letting another thread run
  • I/O tasks don't use the GIL at all
  • Threads create separate interpreters

Answer: A thread releases the GIL while waiting on I/O, letting another thread run. When a thread waits on I/O it releases the GIL, so other threads can make progress.

Do threads or processes share memory by default?

  • Threads share memory; processes have separate memory
  • Processes share memory; threads do not
  • Both share memory
  • Neither shares memory

Answer: Threads share memory; processes have separate memory. Threads share the same memory; processes are isolated and need serialization to share data.

Why is counter += 1 unsafe across multiple threads without a lock?

  • It is always atomic and safe
  • Integers can't be shared
  • It is not atomic — load, add, and store can interleave
  • The GIL prevents all sharing

Answer: It is not atomic — load, add, and store can interleave. counter += 1 is three steps (load, add, store); interleaving threads corrupt the value — a race condition.

What is the recommended way to use a threading.Lock around shared data?

  • lock.acquire() and forget to release
  • with lock:
  • Set lock = True
  • No lock is needed

Answer: with lock:. with lock: auto-acquires and auto-releases, which is the safest pattern.

Compared with threads, how do processes generally start up?

  • Faster, in microseconds
  • Instantly with zero cost
  • At the same speed as threads
  • Slower, in milliseconds (a new interpreter must spawn)

Answer: Slower, in milliseconds (a new interpreter must spawn). Processes are slower to start since each spawns its own Python interpreter and memory.

Which model is best for handling massive numbers of lightweight network tasks?

  • multiprocessing
  • asyncio
  • One thread per task
  • Pure sequential code

Answer: asyncio. asyncio scales to massive lightweight, network-first concurrency on a single thread.

Running heavy number-crunching in 100 threads gives what result in CPython?

  • A ~100x speedup
  • A guaranteed crash
  • No real speedup — the GIL serializes CPU-bound bytecode
  • True parallelism across cores

Answer: No real speedup — the GIL serializes CPU-bound bytecode. The GIL serializes Python bytecode, so CPU-bound threads get no real speedup — use processes.

Continue this course