Concurrency can sound scary when you first hear words like Thread, Process, GIL, Coroutine, Event Loop, Future, Asyncio, etc.
But don't worry!
At the basic level, all of these concepts are about one simple problem:
How can a program handle multiple things efficiently instead of doing everything one by one?
Let's understand everything from scratch.
1. What is a Thread? ๐งต
A thread is a small path of execution inside a program.
Normally, a Python program does things one after another:
Task 1
โ
Task 2
โ
Task 3
โ
Task 4
A thread allows another path of execution to exist inside the same program.
Think of a thread as a worker.
Program
โ
โโโ Thread 1
โโโ Thread 2
โโโ Thread 3
Each thread can work on a different task.
Simple Example
import threading
def say_hello():
print("Hello!")
thread = threading.Thread(target=say_hello)
thread.start()
Important
-
Thread(...)creates a thread. -
start()starts the thread. - Threads are useful when a program has multiple things to handle.
- Threads are especially useful for I/O-bound work, such as network requests and file operations.
2. What is Multithreading? ๐งต
Multithreading means using multiple threads inside one program.
For example:
Program
โ
โโโ Thread 1 โ Download a file
โโโ Thread 2 โ Call an API
โโโ Thread 3 โ Read a file
โโโ Thread 4 โ Query a database
Instead of waiting for one task to completely finish before starting another, multiple tasks can make progress.
Real-World Example
Imagine downloading 4 files.
Without multithreading:
Download File 1
โ
Download File 2
โ
Download File 3
โ
Download File 4
With multithreading:
Thread 1 โ File 1
Thread 2 โ File 2
Thread 3 โ File 3
Thread 4 โ File 4
This can be much more efficient when the tasks spend a lot of time waiting.
3. What is a Process? ๐ญ
A process is basically a running program.
For example, when you run:
python app.py
your operating system starts a process for that program.
A process has its own:
- Memory
- Resources
- Python interpreter
- Threads
A process can contain multiple threads:
Process
โ
โโโ Thread 1
โโโ Thread 2
โโโ Thread 3
The important thing to remember is:
A process is a running program, while a thread is a path of execution inside that process.
4. What is Multiprocessing? ๐ญ
Multiprocessing means using multiple processes.
For example:
Process 1 โ CPU work
Process 2 โ CPU work
Process 3 โ CPU work
Process 4 โ CPU work
Each process has its own memory space.
Python Example
from multiprocessing import Process
def work():
print("Doing some work")
process = Process(target=work)
process.start()
process.join()
Here:
-
Process()creates a new process. -
start()starts it. -
join()waits for it to finish.
Multiprocessing is especially useful for CPU-bound tasks.
5. Thread vs Task โ๏ธ
These two words are easy to confuse.
They are not the same thing.
Task
A task is simply a piece of work that needs to be done.
For example:
Task:
"Download a profile picture"
Thread
A thread is a way of executing work.
Think about it like this:
Task = WHAT needs to be done
Thread = WHERE the work can run
For example:
Task โ Download an image
โ
Thread โ executes the work
In asyncio, tasks work differently. An asyncio task is used to schedule a coroutine to run on the event loop.
6. Multithreading vs Multiprocessing โ๏ธ
This is one of the most important differences.
| Multithreading | Multiprocessing |
|---|---|
| Uses multiple threads | Uses multiple processes |
| Threads belong to one process | Each process is separate |
| Threads share process memory | Processes have separate memory |
| Lightweight | More resource-heavy |
| Great for many I/O tasks | Great for CPU-heavy tasks |
| Affected by the GIL for Python CPU work | Can use multiple CPU cores |
Simple rule
If your program is mostly waiting:
๐ Network
๐ Files
๐๏ธ Database
Think about:
Threading
asyncio
If your program is mostly calculating:
๐งฎ Heavy calculations
๐ผ๏ธ Image processing
๐ฅ Video processing
Think about:
Multiprocessing
7. What is a Lock? ๐
A lock is used when multiple threads are accessing shared data.
Imagine we have:
balance = 100
Two threads try to change it at the same time.
Thread 1 โ withdraw money
Thread 2 โ withdraw money
Both might try to access the balance at the same time.
This can cause incorrect results.
A lock makes sure that only one thread enters a particular section of code at a time.
Example
import threading
lock = threading.Lock()
with lock:
balance -= 50
Think of a lock like a bathroom key:
๐ Someone is inside
โ
Everyone else waits
โ
๐ Person comes out
โ
Next person enters
So:
A lock protects shared data from being changed by multiple threads at the same time.
8. What is the GIL? ๐
GIL stands for:
Global Interpreter Lock
The GIL is a feature of the standard Python implementation, CPython.
The important beginner-level idea is:
The GIL allows only one thread at a time to execute Python bytecode within a CPython process.
This becomes important when we talk about CPU-heavy work.
Imagine:
Thread 1 โ Python code
Thread 2 โ Python code
Thread 3 โ Python code
Thread 4 โ Python code
You might expect all four threads to execute Python code on four CPU cores at exactly the same time.
The GIL prevents that kind of true parallel execution of Python bytecode within one CPython process.
But this does not mean threads are useless!
Threads are still very useful for I/O-bound tasks because programs often spend a lot of time waiting for:
- Network responses
- Database responses
- Files
- External services
9. Why is Threading Still Useful? โ
Suppose a program sends a network request:
Send request
โ
WAIT...
โ
Server responds
During that waiting period, the CPU doesn't need to continuously execute Python code for that request.
Another thread can make progress.
For example:
Thread 1 โ API request โ WAITING
Thread 2 โ process another request
Thread 3 โ read a file
So even with the GIL:
Threading can be very useful for I/O-bound programs.
10. What is Concurrency? โก
Concurrency means handling multiple tasks during overlapping periods of time.
It does not necessarily mean that everything is running at the exact same instant.
For example:
Task A โ โโโ WAIT โโโ
Task B โ โโโ WAIT โโโ
Task C โ โโโโโ
The program switches between tasks and makes progress whenever a task is ready.
A simple way to remember it:
Concurrency = dealing with multiple tasks at once.
11. Why Do We Need Concurrency? ๐ค
Because programs spend a lot of time waiting.
Imagine:
API request โ 3 seconds
Database query โ 2 seconds
File operation โ 4 seconds
If we do everything one by one:
3 + 2 + 4 = 9 seconds
But if the waiting periods overlap:
API request โ โโโโโโโโโ
Database query โ โโโโโ
File operation โ โโโโโโโโโโโ
The total time can be much smaller.
This is why concurrency is useful.
12. Concurrency vs Parallelism ๐
These words sound similar, but they are different.
Concurrency
Multiple tasks are being handled during overlapping periods.
Task A โ โโโ WAIT โโโ
Task B โ โโโ WAIT โโโ
One worker can manage multiple tasks.
Parallelism
Multiple tasks are actually running at the same time.
CPU Core 1 โ Task A
CPU Core 2 โ Task B
CPU Core 3 โ Task C
Easy Example
Concurrency
One person:
Cook ๐ณ
โ
While food cooks, wash dishes
โ
Check food
โ
Prepare salad
Parallelism
Two people:
Person 1 โ Cooking
Person 2 โ Washing dishes
They are doing things at the same time.
13. I/O-Bound vs CPU-Bound Tasks ๐๐งฎ
This is extremely important because it helps us decide which concurrency approach to use.
I/O-Bound Tasks
I/O means Input/Output.
Examples:
- Network requests
- API calls
- Database queries
- Reading files
- Writing files
- Downloading files
These tasks spend a lot of time waiting.
Example:
response = requests.get(url)
The program may spend time waiting for the server.
Think:
I/O-bound = "I'm mostly waiting."
CPU-Bound Tasks
CPU-bound tasks spend most of their time doing calculations.
Examples:
- Heavy mathematical calculations
- Image processing
- Video processing
- Compression
- Large loops
- Some machine-learning workloads
Example:
total = 0
for i in range(1_000_000_000):
total += i
Here the CPU is doing a lot of work.
Think:
CPU-bound = "I'm mostly calculating."
14. Why Does Threading Exist? ๐งต
Computers often have to wait.
For example:
Send API request
โ
WAIT
โ
Response arrives
Instead of leaving the program completely idle, another thread can do something useful.
For example:
Thread 1 โ API request โ waiting
Thread 2 โ Process another request
Thread 3 โ Read a file
Therefore, threading is particularly useful for programs that perform lots of I/O operations.
15. What Does the GIL Change? ๐
Without understanding the GIL, you might think:
"If I create 4 threads, my CPU-heavy Python program will automatically use 4 CPU cores."
In CPython, that's not how it works.
The GIL means that only one thread at a time executes Python bytecode within a process.
Therefore:
CPU-heavy Python work
+
Multiple threads
โ
Not true CPU parallelism
This is one reason multiprocessing is useful for CPU-heavy Python work.
16. Why is Multiprocessing Needed? ๐ญ
Multiprocessing allows us to use multiple processes.
For example:
CPU Core 1 โ Process 1
CPU Core 2 โ Process 2
CPU Core 3 โ Process 3
CPU Core 4 โ Process 4
Each process is separate.
This allows CPU-heavy work to run in parallel across CPU cores.
So:
Multiprocessing is especially useful when the CPU is the bottleneck.
17. Why Does asyncio Exist? ๐
You might ask:
"If threading already exists, why do we need asyncio?"
Good question!
Imagine a server handling thousands of network connections.
Creating a separate thread for every connection can become expensive.
asyncio provides another approach.
Instead of having many threads:
Thread 1 โ waiting
Thread 2 โ waiting
Thread 3 โ waiting
Thread 4 โ waiting
...
we can use an event loop:
One thread
โ
Event Loop
โ
Task 1 โ waiting
Task 2 โ waiting
Task 3 โ waiting
Task 4 โ waiting
When one task is waiting, the event loop can work on another task.
This makes asyncio especially useful for applications with many I/O operations.
18. What is an Event Loop? ๐
The event loop is the heart of asyncio.
It manages asynchronous tasks.
Think of it as a manager:
Event Loop
โ
โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโ
โ โ โ
Task A Task B Task C
waiting ready waiting
The event loop checks:
"Which task is ready to continue?"
If Task A is waiting for a network response:
Task A โ WAITING
the event loop can work on Task B.
Later, when Task A is ready:
Task A โ READY
the event loop can continue it.
19. What is a Coroutine? ๐งฉ
A coroutine is a special function that can pause and resume.
A normal function usually runs from start to finish:
def hello():
print("Hello")
print("World")
An async function can pause while waiting:
async def hello():
print("Hello")
await asyncio.sleep(2)
print("World")
Here the coroutine can pause at:
await asyncio.sleep(2)
and allow another task to run.
Later, it can continue.
Think:
Coroutine
โ
Start
โ
Do some work
โ
WAIT
โ
Pause
โ
Continue later
โ
Finish
20. async and await โจ๏ธ
These are the two most important keywords in Python's async programming.
async
async is used to define an asynchronous function.
async def fetch_data():
print("Fetching data...")
This function is a coroutine function.
await
await tells Python:
"Wait for this asynchronous operation, but allow other async tasks to run while we're waiting."
Example:
import asyncio
async def fetch_data():
print("Start")
await asyncio.sleep(2)
print("Finished")
The important part is:
await asyncio.sleep(2)
The coroutine waits, but the event loop can work on other tasks.
21. What is an Asyncio Task? ๐
An asyncio task is used to schedule a coroutine to run.
Example:
task = asyncio.create_task(fetch_data())
Think about the difference:
Coroutine
โ
The work to be performed
Task
โ
The scheduled work
For example:
async def download():
...
This defines the coroutine.
Then:
task = asyncio.create_task(download())
schedules it as a task.
22. What is a Future? ๐ฎ
A Future represents a result that will be available later.
Think about ordering food ๐
Order placed
โ
Food is not ready
โ
WAIT...
โ
Food ready
โ
Get result
A Future is similar.
It represents something that:
Pending
โ
Waiting
โ
Completed
โ
Result available
You don't usually need to create Futures manually when you're just starting with asyncio.
They are more important when understanding how asynchronous systems work internally.
23. asyncio.gather() ๐ค
asyncio.gather() is used when you want to run multiple asynchronous operations and collect their results.
Example:
import asyncio
async def task1():
await asyncio.sleep(2)
return "Task 1 done"
async def task2():
await asyncio.sleep(1)
return "Task 2 done"
async def main():
results = await asyncio.gather(
task1(),
task2()
)
print(results)
asyncio.run(main())
The two tasks can make progress concurrently.
The results are collected together.
Conceptually:
gather()
โ
โโโโโโโโโโผโโโโโโโโโ
โ โ โ
Task 1 Task 2 Task 3
โ โ โ
Result Result Result
โโโโโโโโโโผโโโโโโโโโ
โ
Results
Use gather() when:
"I have several async operations and I want their results together."
24. asyncio.wait() โณ
asyncio.wait() gives you more control over tasks.
Example:
done, pending = await asyncio.wait(tasks)
You get two groups:
done
โ
Tasks that finished
pending
โ
Tasks that are still running
Conceptually:
Tasks
โ
โโโ Task 1 โ DONE
โโโ Task 2 โ DONE
โโโ Task 3 โ PENDING
โโโ Task 4 โ PENDING
So:
done
contains completed tasks.
And:
pending
contains unfinished tasks.
25. asyncio.wait_for() โฐ
asyncio.wait_for() is used when you want to put a time limit on an async operation.
Example:
result = await asyncio.wait_for(
fetch_data(),
timeout=5
)
This means:
"Wait for
fetch_data(), but don't wait longer than 5 seconds."
If the operation takes too long, it is cancelled and a timeout error occurs.
This is very useful for network requests.
You don't want:
API request
โ
WAIT...
โ
WAIT...
โ
WAIT FOREVER ๐ญ
Instead:
API request
โ
Wait up to 5 seconds
โ
Done or timeout
26. Cancellation โ
Sometimes we start a task but no longer need it.
We can cancel it.
Example:
task = asyncio.create_task(do_work())
task.cancel()
Cancellation basically means:
"Stop this task because we don't need it anymore."
This can happen when:
- A user cancels an operation
- A request times out
- The application is shutting down
- Another task makes the current task unnecessary
Conceptually:
Task
โ
Running
โ
Cancel requested
โ
Cancelled
27. Cooperative Concurrency ๐ค
Cooperative concurrency means that tasks cooperate by giving control back when they are waiting.
This is an important idea in asyncio.
For example:
async def task():
print("Start")
await asyncio.sleep(2)
print("Continue")
When the task reaches:
await asyncio.sleep(2)
it effectively says:
"I'm waiting. You can work on something else."
Then another task can run.
Conceptually:
Task A
โ
await
โ
Pause ๐ค
โ
Task B runs
โ
Task C runs
โ
Task A becomes ready
โ
Task A continues
This is called cooperative concurrency because tasks give up control at appropriate points.
28. How Everything Connects ๐
Now let's connect all the concepts.
CONCURRENCY
โ
โโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโ
โ โ โ
Threading Multiprocessing asyncio
โ โ โ
โ โ โ
Threads Processes Event Loop
โ โ โ
โ โ โ
Good for I/O Good for CPU Good for I/O
โ
โ
Coroutines
โ
โโโโโโโโโโโโผโโโโโโโโโโโ
โ โ โ
async await Tasks
โ
โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโ
โ โ โ
gather() wait() wait_for()
โ
โ
Timeout
29. Which One Should I Use? ๐งญ
Here's the simple decision-making guide.
| Problem | Good option |
|---|---|
| Normal simple program | Regular synchronous code |
| Multiple I/O operations | Threading or asyncio
|
| Lots of network requests |
asyncio is often a great choice |
| File operations | Threading or asyncio
|
| Database operations | Threading or asyncio
|
| Heavy CPU calculations | Multiprocessing |
| Need true CPU parallelism | Multiprocessing |
| Shared data between threads | Lock may be needed |
| Run multiple async operations | asyncio.gather() |
| Check completed/pending tasks | asyncio.wait() |
| Give an async operation a time limit | asyncio.wait_for() |
| Stop an async task | Cancellation |
30. A Real-World Example ๐
Imagine you're building a web application.
A user sends a request:
GET /profile
Your server needs to:
1. Query the database
2. Call another API
3. Download an image
4. Send the response
Most of these operations involve waiting.
With asynchronous programming:
Event Loop
โ
โโโโโโโโโโโโโโโผโโโโโโโโโโโโโโ
โ โ โ
Request A Request B Request C
โ โ โ
await await await
โ โ โ
โ โ โ
Database API Image
waiting waiting waiting
While one request is waiting, the event loop can work on another request.
This is why asynchronous programming is extremely useful for modern network applications.
31. The Most Important Rules to Remember ๐
You don't need to memorize everything at once.
Just remember these basic rules:
๐งต Thread
A path of execution inside a process.
๐งต Multithreading
Using multiple threads in one process.
๐ญ Process
A running program with its own memory and resources.
๐ญ Multiprocessing
Using multiple processes, especially useful for CPU-heavy work.
๐ Lock
Prevents multiple threads from modifying shared data at the same time.
๐ GIL
In CPython, only one thread at a time executes Python bytecode within a process.
โก Concurrency
Handling multiple tasks during overlapping periods.
๐ Parallelism
Multiple tasks actually executing at the same time.
๐ I/O-bound
Mostly waiting for things like networks, databases, and files.
๐งฎ CPU-bound
Mostly doing calculations.
๐ asyncio
Python's asynchronous programming framework for handling many I/O tasks efficiently.
๐ Event Loop
Manages and runs asynchronous tasks.
๐งฉ Coroutine
A function that can pause and continue later.
async
Defines an asynchronous function.
await
Waits for an asynchronous operation while allowing other async tasks to run.
๐ Task
Scheduled asynchronous work.
๐ฎ Future
Represents a result that will be available later.
asyncio.gather()
Runs multiple async operations and collects their results.
asyncio.wait()
Lets you work with completed and pending tasks.
โฐ asyncio.wait_for()
Gives an async operation a time limit.
โ Cancellation
Stops an async task that is no longer needed.
๐ค Cooperative Concurrency
Tasks give control back when they reach points where they can safely wait.
32. Final Mental Model ๐ง
If you remember nothing else, remember this:
YOUR PROGRAM
โ
"I have many things to do"
โ
โ
CONCURRENCY
โ
โโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโ
โ โ โ
THREADING MULTIPROCESSING ASYNCIO
โ โ โ
โ โ โ
Multiple Multiple Event Loop
threads processes โ
โ โ โ
โ โ Coroutines
I/O CPU โ
โ โ async / await
โ โ โ
Waiting Calculating โ
Tasks / Futures
โ
โโโโโโโโโโโโโโผโโโโโโโโโโโโโ
โ โ โ
gather wait wait_for
โ
โ
timeout
The biggest rule is:
I/O-bound โ Threading / Asyncio
CPU-bound โ Multiprocessing
And remember:
Concurrency is about managing multiple tasks.
Parallelism is about actually running multiple tasks at the same time.
Once these two ideas are clear, the rest of Python concurrency becomes much easier to understand.
Top comments (0)