Python Concurrency Explained: Threading vs Asyncio Made Simple
- Posted on October 7, 2025
- Technology
- By MmantraTech
- 59 Views
I tried to write this in simple words so you can just copy and run the code. It might sound a bit casual, but I promise it’s easy to follow.
What is Threading vs Asyncio and when to pick which

In short, threading vs asyncio compares two main ways to handle concurrency in Python. Threading uses OS-level threads — great for blocking I/O tasks or parallel work. Asyncio uses cooperative multitasking, where a single thread manages many async tasks. In my experience, asyncio feels more lightweight for network tasks, while threading is great when you already use blocking code.
Threading
-
Each thread runs separately — like multiple “workers” doing tasks in parallel.
-
OS-level threads, managed by the operating system.
-
Good for I/O-bound tasks (like file download, API calls).
-
Uses more memory since each thread has its own stack.
import threading
def task(n):
print(f"Task {n}")
threading.Thread(target=task, args=(1,)).start()
import threading
import time
class MyThread(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
for i in range(3):
time.sleep(1)
print(f"Thread {self.name} is running ")
ob1 = MyThread("Thread-1")
ob1.start()
ob2 = MyThread("Thread-2")
ob2.start()
info = "Current thread name %s" % threading.current_thread().name
print(info)
ob1.join()
ob2.join()
# Print names of all active threads
for thread in threading.enumerate():
print("Thread name: %s" % thread.name)
# You can also get the number of active threads using:
print("Active thread count:", threading.active_count())
Asyncio
-
Single-threaded but asynchronous — it switches between tasks while waiting (non-blocking).
-
Managed by Python’s event loop, not OS threads.
-
Also great for I/O-bound work, but uses less memory and scales better with thousands of tasks.
import time
def greet(name):
print(f"Hello, {name}!")
time.sleep(2)
print(f"Goodbye, {name}!")
def main():
greet("kamal")
greet("suresh")
greet("rajesh")
main()
import asyncio
async def task(n):
print(f"Task {n}")
asyncio.run(task(1))
import time
import asyncio
async def greet(name):
print(f"Hello, {name}!")
await asyncio.sleep(2)
print(f"Goodbye, {name}!")
async def main():
await asyncio.gather(greet("kamal"), greet("suresh"), greet("rajesh"))
asyncio.run(main())
Ways to use Threading in Python
1. Basic threading.Thread example
This is the simplest way to create and run multiple threads.
# keyword: threading vs asyncio
import threading
import time
def worker(n):
print(f"Worker {n} start")
time.sleep(1)
print(f"Worker {n} done")
threads = []
for i in range(3):
t = threading.Thread(target=worker, args=(i,))
t.start()
threads.append(t)
for t in threads:
t.join()
print("All threads finished")
Each worker runs in parallel using multiple OS threads. You’ll see that all three threads sleep at the same time — perfect for blocking I/O.
2. ThreadPoolExecutor for easier threading
If you have many short blocking tasks, use concurrent.futures.ThreadPoolExecutor
. It manages a pool of threads automatically.
from concurrent.futures import ThreadPoolExecutor
import time
def task(x):
time.sleep(1)
return x * 2
with ThreadPoolExecutor(max_workers=4) as ex:
results = list(ex.map(task, range(6)))
print(results) # [0, 2, 4, 6, 8, 10]
Ways to use Asyncio in Python
Asyncio works differently. It runs everything in a single thread using an event loop and async functions.
1. Basic asyncio.run with async functions
# keyword: threading vs asyncio
import asyncio
async def worker(n):
print(f"Async worker {n} start")
await asyncio.sleep(1)
print(f"Async worker {n} done")
async def main():
await asyncio.gather(*(worker(i) for i in range(3)))
asyncio.run(main())
This code runs three coroutines “together” on one thread. Each waits using await
instead of blocking.
2. Creating tasks with asyncio.create_task
You can create and manage async tasks directly if you want finer control.
import asyncio
async def long_task(n):
await asyncio.sleep(n)
return n
async def main():
tasks = [asyncio.create_task(long_task(i)) for i in (1, 2, 3)]
done, _ = await asyncio.wait(tasks)
print([t.result() for t in done])
asyncio.run(main())
When to use Threading vs Asyncio — simple rules
- Use Threading when your code has blocking I/O (e.g., file operations or non-async libraries).
- Use Asyncio when you handle many network I/O calls with async libraries like
aiohttp
. - Use Multiprocessing if your work is CPU-heavy (since threads share GIL).
Mixing Asyncio and Threading
Sometimes you need both. You can run blocking functions inside an executor from asyncio. This trick helped me when converting old synchronous code to async.
import asyncio
import time
from concurrent.futures import ThreadPoolExecutor
def blocking_work(x):
time.sleep(1)
return x * 2
async def main():
loop = asyncio.get_running_loop()
with ThreadPoolExecutor() as pool:
results = await asyncio.gather(*(loop.run_in_executor(pool, blocking_work, i) for i in range(4)))
print(results)
asyncio.run(main())
Practical comparison
- Threads: Higher memory cost, true OS-level concurrency for I/O.
- Asyncio: Lightweight, single-threaded, best for many concurrent I/O tasks.
- Context switching: Threads depend on OS; Asyncio switches tasks internally — much faster.
Conclusion
To sum up, threading vs asyncio in Python depends on your task type. Threading is perfect for blocking I/O and easy migration, while asyncio shines for handling thousands of async network calls. In my experience, I start with threading for simplicity, and move to asyncio when I need more scale and speed.
Write a Response