Published on

⚡ Understanding Synchronous, Multiprocessing, Multithreading, and Asynchronous in Python

Authors

⚡ Understanding Synchronous, Multiprocessing, Multithreading, and Asynchronous in Python

When building high-performance applications, the way your code executes tasks matters. Should you run them one after another? In parallel? On multiple threads? Or using async I/O?

In this post, we’ll break down Synchronous, Multiprocessing, Multithreading, and Asynchronous approaches — what they are, how they work, when to use them, and their pros and cons.


⏳ 1. Synchronous Execution

Definition: Tasks are executed one after another in the order they appear. Each task must finish before the next one starts.

🔍 How it Works

  1. Start Task 1 → wait until it finishes
  2. Start Task 2 → wait until it finishes
  3. Repeat…

Think of it as standing in a single checkout line — each person (task) waits for the one before them to finish.

📜 Example (Python)

import time

def task(name):
    print(f"Starting {name}")
    time.sleep(1)
    print(f"Finished {name}")

for i in range(3):
    task(f"Task {i+1}")

🖥 2. Multiprocessing

Definition: Runs tasks in separate processes, each with its own Python interpreter and memory space. Ideal for CPU-bound tasks because it bypasses Python's Global Interpreter Lock (GIL).

🔍 How it Works

  • Each process executes independently
  • Can use multiple CPU cores simultaneously
  • Data sharing between processes is slower (requires IPC)

Think of it as having multiple cashiers — each serves a customer (task) independently.

📜 Example (Python)

from multiprocessing import Process
import time

def task(name):
    print(f"Starting {name}")
    time.sleep(1)
    print(f"Finished {name}")

if __name__ == "__main__":
    processes = [Process(target=task, args=(f"Task {i+1}",)) for i in range(3)]
    for p in processes: p.start()
    for p in processes: p.join()

🧵 3. Multithreading

Definition: Runs multiple threads within the same process. Good for I/O-bound tasks (e.g., file, network, database calls), but CPU-bound threads are still limited by the GIL.

🔍 How it Works

  • All threads share the same memory space
  • Context switching is faster than multiprocessing
  • Best for overlapping waiting times (I/O)

Think of it as one cashier but multiple baggers working at the same checkout — while the cashier scans, others prepare items.

📜 Example (Python)

from threading import Thread
import time

def task(name):
    print(f"Starting {name}")
    time.sleep(1)
    print(f"Finished {name}")

threads = [Thread(target=task, args=(f"Task {i+1}",)) for i in range(3)]
for t in threads: t.start()
for t in threads: t.join()

⚡ 4. Asynchronous (Async I/O)

Definition: Uses an event loop to schedule tasks without blocking. Best for I/O-bound tasks where you can switch to another task while waiting.

🔍 How it Works

  • A single thread runs an event loop
  • Tasks yield control when waiting (e.g., network response)
  • Extremely efficient for handling many connections

Think of it as a single cashier who takes multiple orders at once, starting one, then moving to the next while waiting for change.

📜 Example (Python)

import asyncio

async def task(name):
    print(f"Starting {name}")
    await asyncio.sleep(1)
    print(f"Finished {name}")

async def main():
    await asyncio.gather(*(task(f"Task {i+1}") for i in range(3)))

asyncio.run(main())

📊 Comparison Table

FeatureSynchronousMultiprocessingMultithreadingAsynchronous
Execution ModelSequentialParallel (multi-core)Concurrent (single-core for CPU-bound)Concurrent (event loop)
Best forSimplicityCPU-bound tasksI/O-bound tasksI/O-bound tasks
Bypasses GILNo✅ Yes❌ No❌ No
Memory UsageLowHighLowVery Low
ComplexityLowMediumMediumHigh

🌐 Real-World Example: Downloading Multiple Files Optimally

File downloading is an I/O-bound task — most of the time is spent waiting for the server to respond, not crunching CPU numbers.

If you download files synchronously, you’ll wait for each one to finish before starting the next. Instead, you can overlap network waits using Multithreading or Asynchronous I/O.

⏳ Synchronous (Slow)

import requests
import time

urls = [
    "https://example.com/file1.zip",
    "https://example.com/file2.zip",
    "https://example.com/file3.zip",
]

start = time.time()
for url in urls:
    r = requests.get(url)
    print(f"Downloaded {url} ({len(r.content)} bytes)")
print(f"Synchronous: {time.time() - start:.2f} seconds")

🧵 Multithreading (Faster for I/O-bound)

from threading import Thread
import requests
import time

urls = [
    "https://example.com/file1.zip",
    "https://example.com/file2.zip",
    "https://example.com/file3.zip",
]

def download(url):
    r = requests.get(url)
    print(f"Downloaded {url} ({len(r.content)} bytes)")

start = time.time()
threads = [Thread(target=download, args=(url,)) for url in urls]
for t in threads: t.start()
for t in threads: t.join()
print(f"Multithreading: {time.time() - start:.2f} seconds")

⚡ Asynchronous (Efficient for Many Files)

import aiohttp
import asyncio
import time

urls = [
    "https://example.com/file1.zip",
    "https://example.com/file2.zip",
    "https://example.com/file3.zip",
]

async def download(session, url):
    async with session.get(url) as resp:
        content = await resp.read()
        print(f"Downloaded {url} ({len(content)} bytes)")

async def main():
    async with aiohttp.ClientSession() as session:
        await asyncio.gather(*(download(session, url) for url in urls))

start = time.time()
asyncio.run(main())
print(f"Asynchronous: {time.time() - start:.2f} seconds")

Key Insight:

  • For a few downloads, Multithreading is simpler to implement.
  • For dozens or hundreds of concurrent downloads, Asynchronous I/O scales better in terms of memory and CPU usage.

📌 When to Use Which?

  • Synchronous: Small scripts or when simplicity is more important than speed
  • Multiprocessing: Heavy computations (e.g., image processing, ML training)
  • Multithreading: Multiple I/O-bound operations (e.g., web scraping)
  • Asynchronous: High-concurrency servers, network apps, chatbots

🔚 Recap

Choosing the right execution model depends on task type:

  • CPU-bound → Multiprocessing
  • I/O-bound → Multithreading or Async
  • Simple tasks → Synchronous

🔜 Coming Next

Next in this performance and concurrency subseries: Thread Pools & Process Pools — simplifying parallel execution in Python with concurrent.futures.

Stay curious and keep exploring 👇

🙏 Acknowledgments

Special thanks to ChatGPT for enhancing this post with examples, diagrams, and comparisons.