- What is multi-tasking β doing multiple things at once
- Threading β lightweight concurrent execution
- Multiprocessing β using multiple CPU cores
- Asynchronous programming β event-driven concurrency
- When to use each β choosing the right approach
What is Multi Tasking?
Multi-tasking means doing multiple things at the same time. In programming, it means executing multiple tasks concurrently, making your program faster and more efficient.
π³ Think of it like a restaurant kitchen.
Single tasking: One chef does everything β takes orders, cooks, cleans. Everything is slow.
Multi-tasking: Multiple chefs work simultaneously β one takes orders, another cooks, another cleans. Everything is fast and efficient!
Threading
Lightweight threads for I/O-bound tasks
Multiprocessing
Processes for CPU-bound tasks
Asynchronous
Event-driven for high concurrency
Combined
Mix approaches for best results
π‘ Key concept: Multi-tasking makes your program faster by utilizing system resources more efficiently.
Multi-Tasking in Action
# ============================================================
# MULTI-TASKING EXAMPLE
# ============================================================
import threading
import time
def task1():
print("Task 1: Starting...")
time.sleep(2)
print("Task 1: Finished!")
def task2():
print("Task 2: Starting...")
time.sleep(2)
print("Task 2: Finished!")
def task3():
print("Task 3: Starting...")
time.sleep(2)
print("Task 3: Finished!")
print("=" * 40)
print("MULTI-TASKING IN ACTION")
print("=" * 40)
print("\nRunning tasks in parallel...")
start = time.time()
# Create threads
t1 = threading.Thread(target=task1)
t2 = threading.Thread(target=task2)
t3 = threading.Thread(target=task3)
# Start threads
t1.start()
t2.start()
t3.start()
# Wait for threads
t1.join()
t2.join()
t3.join()
end = time.time()
print(f"\nAll tasks completed in {end - start:.2f} seconds")
print("""
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β OBSERVATIONS: β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ Tasks run simultaneously β
β β’ Total time = time of slowest task (not sum of all) β
β β’ Much faster than single tasking β
β β’ More complex than single tasking β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
""")
Key point: Multi-tasking runs tasks in parallel, reducing total execution time.
Quick Check: What is multi-tasking? (Answer: Doing multiple things at the same time)
Threading
Lightweight Concurrent Execution
Threading is the most common way to do multi-tasking in Python. Threads are lightweight and share memory, making them efficient for I/O-bound tasks.
# ============================================================
# THREADING EXAMPLE
# ============================================================
import threading
import time
import random
# ============================================================
# BASIC THREADING
# ============================================================
def download_file(file_name):
"""Simulate downloading a file"""
thread_name = threading.current_thread().name
size = random.randint(1, 5)
print(f"[{thread_name}] Downloading {file_name} ({size}MB)")
for progress in range(0, 101, 20):
time.sleep(random.uniform(0.1, 0.3))
print(f"[{thread_name}] {file_name}: {progress}%")
print(f"[{thread_name}] {file_name}: Complete!")
# Create and start threads
files = ["file1.mp4", "file2.pdf", "file3.jpg", "file4.exe"]
threads = []
for file_name in files:
t = threading.Thread(target=download_file, args=(file_name,))
threads.append(t)
t.start()
# Wait for all threads
for t in threads:
t.join()
print("All downloads complete!")
# ============================================================
# THREADING WITH POOL
# ============================================================
from concurrent.futures import ThreadPoolExecutor
print("\n" + "=" * 40)
print("THREAD POOL EXECUTOR")
print("=" * 40)
def process_data(data_id):
"""Process data with ThreadPoolExecutor"""
time.sleep(random.uniform(0.2, 0.5))
return f"Data {data_id} processed"
# Use ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=4) as executor:
results = executor.map(process_data, range(10))
for result in results:
print(result)
print("""
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β THREADING ADVANTAGES: β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ Lightweight (low memory overhead) β
β β’ Share memory (fast communication) β
β β’ Great for I/O-bound tasks β
β β’ Easy to implement β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β THREADING DISADVANTAGES: β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ GIL limits CPU-bound performance β
β β’ Race conditions (need synchronization) β
β β’ Not for CPU-intensive work β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
""")
Threading key points:
- Lightweight β low memory overhead
- Shared memory β fast communication
- I/O-bound β great for waiting tasks
- GIL limited β not for CPU work
Quick Check: What type of tasks are threads best for? (Answer: I/O-bound tasks)
Multiprocessing
Using Multiple CPU Cores
Multiprocessing creates separate processes, each with its own memory space. This bypasses the GIL and allows true parallelism for CPU-bound tasks.
# ============================================================
# MULTIPROCESSING EXAMPLE
# ============================================================
from multiprocessing import Pool, Process
import time
import math
# ============================================================
# CPU-BOUND TASK
# ============================================================
def cpu_intensive_task(n):
"""Calculate factorial (CPU-intensive)"""
result = math.factorial(n)
return f"Factorial of {n} is {result}"
def run_with_processes():
"""Run tasks with processes"""
numbers = [100000, 120000, 140000, 160000]
print("\nRunning with processes (parallel):")
start = time.time()
with Pool(processes=4) as pool:
results = pool.map(cpu_intensive_task, numbers)
end = time.time()
print(f"Time: {end - start:.2f}s")
for result in results:
print(result)
def run_with_threads():
"""Run tasks with threads"""
numbers = [100000, 120000, 140000, 160000]
print("\nRunning with threads (GIL limited):")
start = time.time()
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(cpu_intensive_task, numbers))
end = time.time()
print(f"Time: {end - start:.2f}s")
for result in results:
print(result)
# ============================================================
# COMPARE PERFORMANCE
# ============================================================
print("=" * 60)
print("PROCESSES vs THREADS - CPU-BOUND TASK")
print("=" * 60)
print("""
This task is CPU-intensive (calculating factorials).
Threads are limited by the GIL, processes are not.
Expected: Processes will be faster than threads.
""")
run_with_processes()
run_with_threads()
print("""
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β MULTIPROCESSING ADVANTAGES: β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ True parallelism on multi-core CPUs β
β β’ Bypasses the GIL β
β β’ Great for CPU-bound tasks β
β β’ Isolated (one crash doesn't affect others) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β MULTIPROCESSING DISADVANTAGES: β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ Higher memory usage β
β β’ Slower to create processes β
β β’ Communication is slower (IPC) β
β β’ More complex than threading β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
""")
Multiprocessing key points:
- True parallelism β uses multiple cores
- Bypasses GIL β no limitation
- CPU-bound β great for calculations
- Isolated β processes don't share memory
Quick Check: What type of tasks are processes best for? (Answer: CPU-bound tasks)
Asynchronous Programming
Event-Driven Concurrency
Asynchronous programming uses an event loop to handle many concurrent operations without threads. It's great for high-concurrency I/O applications.
# ============================================================
# ASYNCHRONOUS PROGRAMMING
# ============================================================
import asyncio
import random
import time
# ============================================================
# BASIC ASYNC EXAMPLE
# ============================================================
async def async_task(task_name, delay):
"""Asynchronous task"""
print(f"{task_name}: Starting...")
await asyncio.sleep(delay) # Non-blocking wait
print(f"{task_name}: Finished after {delay}s")
return f"{task_name} result"
async def run_async_tasks():
"""Run multiple async tasks concurrently"""
print("Running async tasks...")
start = time.time()
# Create tasks
tasks = [
async_task("Task A", 2),
async_task("Task B", 1),
async_task("Task C", 3)
]
# Run concurrently
results = await asyncio.gather(*tasks)
end = time.time()
print(f"All tasks completed in {end - start:.2f}s")
for result in results:
print(result)
# ============================================================
# WEB SCRAPING WITH ASYNC
# ============================================================
async def fetch_page(url):
"""Simulate fetching a web page"""
print(f"Fetching: {url}")
await asyncio.sleep(random.uniform(0.5, 1.5))
return f"Content from {url}"
async def scrape_sites():
"""Scrape multiple sites concurrently"""
urls = [
"example.com/page1",
"example.com/page2",
"example.com/page3",
"example.com/page4",
"example.com/page5"
]
print(f"Scraping {len(urls)} sites...")
start = time.time()
tasks = [fetch_page(url) for url in urls]
results = await asyncio.gather(*tasks)
end = time.time()
print(f"All sites scraped in {end - start:.2f}s")
for result in results[:3]:
print(f" {result}")
if len(results) > 3:
print(f" ... and {len(results)-3} more")
# ============================================================
# RUN ASYNC CODE
# ============================================================
print("=" * 60)
print("ASYNCHRONOUS PROGRAMMING")
print("=" * 60)
# Run async tasks
asyncio.run(run_async_tasks())
print("\n" + "=" * 40)
asyncio.run(scrape_sites())
print("""
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ASYNCHRONOUS ADVANTAGES: β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ High concurrency (thousands of connections) β
β β’ Low overhead (no threads) β
β β’ Great for I/O-bound, high-volume tasks β
β β’ Non-blocking operations β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β ASYNCHRONOUS DISADVANTAGES: β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β’ More complex to write β
β β’ Requires async/await throughout β
β β’ Not for CPU-bound tasks β
β β’ Learning curve is steeper β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
""")
Asynchronous key points:
- High concurrency β thousands of connections
- Low overhead β no threads
- Non-blocking β efficient waiting
- Complex β requires async/await
Quick Check: What is asynchronous programming best for? (Answer: High-concurrency I/O applications)
Method Comparison
Threading vs Multiprocessing vs Async
| Feature | Threading | Multiprocessing | Async |
|---|---|---|---|
| Memory | Shared | Separate | Shared |
| GIL | Affected | Not Affected | Not Affected |
| Best For | I/O-bound | CPU-bound | I/O-bound (high concurrency) |
| Complexity | Low | Medium | High |
| Overhead | Low | High | Low |
| Communication | Shared memory | IPC | Async/await |
Comparison summary:
- Threading β I/O-bound, simple, shared memory
- Multiprocessing β CPU-bound, bypasses GIL
- Async β high-concurrency I/O, non-blocking
Quick Check: Which method bypasses the GIL? (Answer: Multiprocessing)
When to Use Which
Choosing the Right Approach
# ============================================================
# DECISION GUIDE
# ============================================================
print("""
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β USE THREADING WHEN: β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 1. Tasks are I/O-bound (network, disk, database) β
β 2. Need shared memory β
β 3. Low overhead is important β
β 4. Moderate number of concurrent tasks β
β β
β Examples: Download manager, web scraper, GUI applications β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β USE MULTIPROCESSING WHEN: β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 1. Tasks are CPU-bound (calculations) β
β 2. Need to use multiple CPU cores β
β 3. Bypass the GIL β
β 4. Tasks are independent β
β β
β Examples: ML training, data processing, image processing β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β USE ASYNC WHEN: β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β 1. Very high concurrency (thousands of connections) β
β 2. I/O-bound with many small operations β
β 3. Need low overhead β
β 4. Non-blocking operations β
β β
β Examples: Web servers, chat applications, API gateways β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
""")
# ============================================================
# FLOW CHART
# ============================================================
print("=" * 40)
print("DECISION FLOW CHART")
print("=" * 40)
print("""
Is task CPU-bound or I/O-bound?
|
v
βββββββββββββ΄ββββββββββββ
β β
CPU-BOUND I/O-BOUND
β β
v v
Is multicore How many concurrent
needed? connections?
β β
v v
βββββ΄ββββ βββββββββ΄ββββββββ
β β β β
Yes No Few/Medium Many
β β β β
v v v v
Processes β Threading Async
β β β
βββββββββββββ΄ββββββββββββββββ
β
v
Consider hybrid approach
""")
Decision guide summary:
- CPU-bound β Multiprocessing
- I/O-bound (few tasks) β Threading
- I/O-bound (many tasks) β Async
- Mixed β Hybrid approach
Quick Check: What should you use for CPU-bound tasks? (Answer: Multiprocessing)
Real-World Example
Complete Multi-Tasking Application
# ============================================================
# REAL-WORLD: MULTI-TASKING APPLICATION
# ============================================================
import threading
import multiprocessing
import asyncio
import time
import random
class MultiTaskingApp:
"""Demonstrate all multi-tasking approaches"""
def __init__(self):
self.results = []
# ============================================================
# THREADING EXAMPLE
# ============================================================
def threaded_work(self, worker_id):
"""I/O-bound work using threads"""
time.sleep(random.uniform(0.3, 0.7))
result = f"Thread {worker_id}: completed I/O work"
self.results.append(result)
return result
def run_threading(self, num_workers=5):
"""Run multiple threads"""
print("\n1. THREADING:")
threads = []
self.results = []
start = time.time()
for i in range(num_workers):
t = threading.Thread(target=self.threaded_work, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
end = time.time()
print(f" Completed {num_workers} tasks in {end - start:.2f}s")
print(f" Results: {len(self.results)}")
# ============================================================
# MULTIPROCESSING EXAMPLE
# ============================================================
@staticmethod
def cpu_work(n):
"""CPU-bound work"""
result = sum(i*i for i in range(n))
return f"Process: sum of squares = {result}"
def run_multiprocessing(self, num_workers=4):
"""Run multiple processes"""
print("\n2. MULTIPROCESSING:")
start = time.time()
with multiprocessing.Pool(processes=num_workers) as pool:
numbers = [1000000 + i * 100000 for i in range(num_workers)]
results = pool.map(self.cpu_work, numbers)
end = time.time()
print(f" Completed {num_workers} CPU tasks in {end - start:.2f}s")
for result in results[:2]:
print(f" {result[:50]}...")
# ============================================================
# ASYNC EXAMPLE
# ============================================================
async def async_work(self, task_id):
"""Async I/O work"""
await asyncio.sleep(random.uniform(0.2, 0.5))
return f"Async {task_id}: completed"
async def run_async(self, num_tasks=10):
"""Run async tasks"""
print("\n3. ASYNCHRONOUS:")
start = time.time()
tasks = [self.async_work(i) for i in range(num_tasks)]
results = await asyncio.gather(*tasks)
end = time.time()
print(f" Completed {num_tasks} async tasks in {end - start:.2f}s")
print(f" Results: {len(results)}")
# ============================================================
# RUN ALL
# ============================================================
def run_all(self):
"""Run all multi-tasking approaches"""
print("=" * 60)
print("MULTI-TASKING DEMONSTRATION")
print("=" * 60)
self.run_threading(5)
self.run_multiprocessing(4)
asyncio.run(self.run_async(10))
print("\n" + "=" * 60)
print("SUMMARY")
print("=" * 60)
print("""
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Approach β Best For β Complexity β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Threading β I/O-bound, moderate tasks β Low β
β Multiprocessingβ CPU-bound, heavy tasks β Medium β
β Async β High-concurrency I/O β High β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Choose based on your specific needs!
""")
# ============================================================
# RUN THE APPLICATION
# ============================================================
if __name__ == "__main__":
app = MultiTaskingApp()
app.run_all()
Real-world example key points:
- All approaches β demonstrates threading, multiprocessing, async
- Different use cases β I/O vs CPU vs high concurrency
- Performance comparison β see the differences
- Practical β real-world scenarios
Quick Check: What determines which multi-tasking approach to use? (Answer: The type of task - CPU-bound, I/O-bound, or high-concurrency I/O)
Best Practices
Multi-Tasking Best Practices
# ============================================================
# MULTI-TASKING BEST PRACTICES
# ============================================================
print("1. CHOOSE THE RIGHT APPROACH")
print(" - I/O-bound β Threading or Async")
print(" - CPU-bound β Multiprocessing")
print(" - Mixed β Hybrid approach")
print("\n2. USE POOLS FOR MANY TASKS")
print(" - ThreadPoolExecutor for threads")
print(" - ProcessPoolExecutor for processes")
print(" - Async for high concurrency")
print("\n3. HANDLE SHARED DATA CAREFULLY")
print(" - Use locks for threads")
print(" - Use queues for processes")
print(" - Async doesn't share data")
print("\n4. LIMIT RESOURCES")
print(" - Don't create too many threads")
print(" - Don't create too many processes")
print(" - Use appropriate pool sizes")
print("\n5. HANDLE ERRORS")
print(" - Catch exceptions in threads")
print(" - Handle errors in processes")
print(" - Try/except in async tasks")
print("\n6. USE CONTEXT MANAGERS")
print(" - with Pool() as pool")
print(" - with ThreadPoolExecutor()")
print(" - Proper cleanup")
print("\n7. TEST THOROUGHLY")
print(" - Race conditions can be hard to find")
print(" - Test with different loads")
print(" - Use debugging tools")
print("\n8. MONITOR PERFORMANCE")
print(" - Measure execution time")
print(" - Check resource usage")
print(" - Optimize when needed")
Best practices summary:
- Choose right approach β based on task type
- Use pools β manage resources
- Handle shared data β prevent race conditions
- Test thoroughly β concurrency bugs are tricky
Quick Check: What should you use to manage many threads? (Answer: ThreadPoolExecutor)
Try It Yourself
Experiment with multi-tasking approaches in the editor below.
MULTI TASKING - PRACTICE
========================================
1. SINGLE TASKING (Sequential)
----------------------------------------
Task A: Starting
Task A: Completed
Task B: Starting
Task B: Completed
Task C: Starting
Task C: Completed
Total time: 1.01s
2. THREADING (Parallel)
----------------------------------------
Starting threads in parallel...
Thread A: Starting
Thread B: Starting
Thread C: Starting
Thread C: Completed
Thread A: Completed
Thread B: Completed
Total time: 0.01s
3. MULTIPROCESSING (CPU Bound)
----------------------------------------
Running CPU tasks in parallel...
Total time: 0.40s
4. MULTI-TASKING COMPARISON
----------------------------------------
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Approach β Best For β Speed β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Single Tasking β Simple tasks β Slowest β
β Threading β I/O-bound β Faster β
β Multiprocessingβ CPU-bound β Fastest β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Multi-tasking makes your program faster!
You've Got It!
You now understand multi-tasking in Python. You know threading, multiprocessing, and async programming, and when to use each.
Quick Quiz
Test what you've learned:
Frequently Asked Questions
What is the difference between threading and multiprocessing?
What is the GIL and why does it matter?
What is a common interview question about multi-tasking?
Can I use multiple approaches together?
What is the easiest multi-tasking approach?
Where to Go From Here
Now that you understand multi-tasking, check out these related topics:
Thread Synchronization
Learn how to safely share data between threads.
Learn More βCreating Threads
Deep dive into creating and managing threads.
Learn More βThreads Assignments
Practice your threading skills with exercises.
Practice Now β