Skip to main content
โšก Calmops

Async Programming: Complete Guide to Concurrency and Parallelism

Introduction

Asynchronous programming has become essential for building high-performance applications. Whether you’re building web servers, data pipelines, or real-time systems, understanding concurrency and parallelism helps you design efficient systems that can handle thousands of simultaneous operations.

This comprehensive guide covers asynchronous programming fundamentals, the difference between concurrency and parallelism, and practical implementation patterns in Python and JavaScript. You’ll learn when to use async programming and how to avoid common pitfalls.

Async programming is particularly essential for I/O-bound applications like web servers, API clients, and database operations. By understanding these concepts, you can dramatically improve your application’s throughput and responsiveness.

Understanding Concurrency vs Parallelism

The Key Distinction

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   Concurrency vs Parallelism                            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                       โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚   โ”‚                    Concurrency                                  โ”‚   โ”‚
โ”‚   โ”‚  โ€ข Handling multiple tasks at once                          โ”‚   โ”‚
โ”‚   โ”‚  โ€ข Not necessarily running simultaneously                     โ”‚   โ”‚
โ”‚   โ”‚  โ€ข Task switching creates illusion of progress              โ”‚   โ”‚
โ”‚   โ”‚  โ€ข Great for I/O-bound tasks                                โ”‚   โ”‚
โ”‚   โ”‚                                                             โ”‚   โ”‚
โ”‚   โ”‚  Example: Restaurant with one waiter serving multiple tables โ”‚   โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                                                                       โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚   โ”‚                    Parallelism                                โ”‚   โ”‚
โ”‚   โ”‚  โ€ข Running multiple tasks simultaneously                    โ”‚   โ”‚
โ”‚   โ”‚  โ€ข Requires multiple CPU cores                             โ”‚   โ”‚
โ”‚   โ”‚  โ€ข True simultaneous execution                              โ”‚   โ”‚
โ”‚   โ”‚  โ€ข Great for CPU-bound tasks                               โ”‚   โ”‚
โ”‚   โ”‚                                                             โ”‚   โ”‚
โ”‚   โ”‚  Example: Multiple chefs cooking different dishes          โ”‚   โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                                                                       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

When to Use Each

Scenario Approach Example
Network requests Concurrency Fetching multiple APIs
CPU computation Parallelism Image processing
Database queries Concurrency Multiple queries
File I/O Concurrency Reading multiple files
Web servers Both Handling many requests

Python Asyncio

Getting Started

import asyncio


async def fetch_data(url: str, delay: float = 1.0) -> dict:
    """Simulate fetching data from URL."""
    await asyncio.sleep(delay)  # Simulate network call
    return {"url": url, "data": f"Data from {url}"}


async def main():
    """Main async function."""
    
    # Sequential execution (slow)
    print("Sequential:")
    result1 = await fetch_data("http://api1.com")
    result2 = await fetch_data("http://api2.com")
    result3 = await fetch_data("http://api3.com")
    print(f"Results: {[r['url'] for r in [result1, result2, result3]]}")
    
    # Concurrent execution with gather (fast)
    print("\nConcurrent:")
    results = await asyncio.gather(
        fetch_data("http://api1.com"),
        fetch_data("http://api2.com"),
        fetch_data("http://api3.com")
    )
    print(f"Results: {[r['url'] for r in results]}")


# Run the async main
asyncio.run(main())

Async Context Managers

class AsyncDatabaseConnection:
    """Async database connection context manager."""
    
    async def __aenter__(self):
        """Connect to database."""
        await asyncio.sleep(0.1)  # Simulate connection
        print("Connected to database")
        return self
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Close database connection."""
        await asyncio.sleep(0.05)  # Simulate cleanup
        print("Disconnected from database")
        return False  # Don't suppress exceptions


async def main():
    async with AsyncDatabaseConnection() as conn:
        # Perform database operations
        result = await conn.query("SELECT * FROM users")
        print(f"Query result: {result}")


# Using async context manager
async def database_example():
    async with AsyncDatabaseConnection() as db:
        await db.execute("INSERT INTO logs VALUES ('test')")
        results = await db.query("SELECT * FROM logs")

Task Management

async def task_with_timeout():
    """Execute task with timeout."""
    try:
        result = await asyncio.wait_for(
            fetch_data("http://slow-api.com"),
            timeout=5.0
        )
        return result
    except asyncio.TimeoutError:
        print("Task timed out!")
        return None


async def multiple_tasks():
    """Manage multiple tasks."""
    
    # Create tasks
    task1 = asyncio.create_task(fetch_data("http://api1.com"))
    task2 = asyncio.create_task(fetch_data("http://api2.com"))
    task3 = asyncio.create_task(fetch_data("http://api3.com"))
    
    # Wait for all tasks
    done, pending = await asyncio.wait(
        [task1, task2, task3],
        return_when=asyncio.FIRST_COMPLETED
    )
    
    print(f"Completed: {len(done)}, Pending: {len(pending)}")
    
    # Wait for remaining
    remaining = await asyncio.gather(*pending)
    return done, remaining


async def task_with_callbacks():
    """Add callbacks to tasks."""
    
    def callback(task):
        try:
            result = task.result()
            print(f"Task completed: {result['url']}")
        except Exception as e:
            print(f"Task failed: {e}")
    
    task = asyncio.create_task(fetch_data("http://api.com"))
    task.add_done_callback(callback)
    
    await task

Error Handling

async def safe_gather():
    """Handle errors in gather."""
    
    async def risky_task(name: str, should_fail: bool = False):
        await asyncio.sleep(0.5)
        if should_fail:
            raise ValueError(f"Task {name} failed!")
        return f"Task {name} succeeded"
    
    # Option 1: return_exceptions=True
    results = await asyncio.gather(
        risky_task("task1"),
        risky_task("task2", should_fail=True),
        risky_task("task3"),
        return_exceptions=True
    )
    
    for i, result in enumerate(results):
        if isinstance(result, Exception):
            print(f"Task {i+1} failed: {result}")
        else:
            print(f"Task {i+1} succeeded: {result}")
    
    # Option 2: asyncio.wait
    tasks = [
        asyncio.create_task(risky_task("task1")),
        asyncio.create_task(risky_task("task2", should_fail=True)),
        asyncio.create_task(risky_task("task3"))
    ]
    
    done, pending = await asyncio.wait(tasks)
    
    for task in done:
        if task.exception():
            print(f"Exception: {task.exception()}")
        else:
            print(f"Result: {task.result()}")

JavaScript Async/Await

Modern Async Patterns

// Basic async/await
async function fetchData(url) {
    try {
        const response = await fetch(url);
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Fetch failed:', error);
        throw error;
    }
}

// Concurrent requests
async function fetchAll(urls) {
    // Promise.all for concurrent execution
    const promises = urls.map(url => fetch(url).then(r => r.json()));
    const results = await Promise.all(promises);
    return results;
}

// Promise.allSettled for error handling
async function fetchWithSettled(urls) {
    const results = await Promise.allSettled(
        urls.map(url => fetch(url).then(r => r.json()))
    );
    
    results.forEach((result, index) => {
        if (result.status === 'fulfilled') {
            console.log(`URL ${index}:`, result.value);
        } else {
            console.log(`URL ${index} failed:`, result.reason);
        }
    });
}

// Promise.race - first to complete
async function fetchWithTimeout(url, timeout = 5000) {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), timeout);
    
    try {
        const response = await fetch(url, { signal: controller.signal });
        return await response.json();
    } finally {
        clearTimeout(timeoutId);
    }
}

Async Iterators

// Async iterator for streaming data
async function* fetchPages(baseUrl) {
    let page = 1;
    let hasMore = true;
    
    while (hasMore) {
        const response = await fetch(`${baseUrl}?page=${page}`);
        const data = await response.json();
        
        if (data.length === 0) {
            hasMore = false;
        } else {
            yield data;
            page++;
        }
    }
}

// Using async iterator
async function processAllPages() {
    for await (const page of fetchPages('https://api.example.com/items')) {
        console.log(`Processing ${page.length} items`);
        // Process each page
    }
}

// Async generator for real-time data
async function* streamEvents() {
    const eventSource = new EventSource('/api/events');
    
    try {
        while (true) {
            const event = await new Promise((resolve, reject) => {
                eventSource.onmessage = (e) => resolve(e.data);
                eventSource.onerror = (e) => reject(e);
            });
            yield JSON.parse(event);
        }
    } finally {
        eventSource.close();
    }
}

Advanced Patterns

// Rate limiting with async
class RateLimiter {
    constructor(maxRequests, timeWindow) {
        this.maxRequests = maxRequests;
        this.timeWindow = timeWindow;
        this.requests = [];
    }
    
    async acquire() {
        const now = Date.now();
        this.requests = this.requests.filter(t => now - t < this.timeWindow);
        
        if (this.requests.length >= this.maxRequests) {
            const oldest = this.requests[0];
            const waitTime = this.timeWindow - (now - oldest);
            await new Promise(r => setTimeout(r, waitTime));
            return this.acquire();
        }
        
        this.requests.push(now);
    }
}

// Using rate limiter
const limiter = new RateLimiter(10, 1000); // 10 requests per second

async function fetchWithRateLimit(url) {
    await limiter.acquire();
    return fetch(url);
}

// Retry with exponential backoff
async function fetchWithRetry(url, maxRetries = 3) {
    for (let i = 0; i < maxRetries; i++) {
        try {
            return await fetch(url);
        } catch (error) {
            if (i === maxRetries - 1) throw error;
            const delay = Math.pow(2, i) * 1000;
            await new Promise(r => setTimeout(r, delay));
        }
    }
}

Threading vs Async

When to Use Threads

import threading
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor


def cpu_intensive_task(n):
    """CPU-bound computation."""
    result = sum(i * i for i in range(n))
    return result


# Use ProcessPoolExecutor for CPU-bound tasks
def parallel_cpu_work():
    with ProcessPoolExecutor(max_workers=4) as executor:
        futures = [executor.submit(cpu_intensive_task, 10**7) for _ in range(4)]
        results = [f.result() for f in futures]
    return results


# Use ThreadPoolExecutor for I/O-bound tasks
def io_bound_task(url):
    import requests
    return requests.get(url).status_code


def parallel_io_work():
    urls = ["http://example.com" for _ in range(10)]
    with ThreadPoolExecutor(max_workers=10) as executor:
        results = list(executor.map(io_bound_task, urls))
    return results

Async with Threading

import asyncio
from concurrent.futures import ThreadPoolExecutor


def blocking_io_operation():
    """Blocking operation that needs to run in thread."""
    import time
    time.sleep(1)  # This blocks!
    return "Done"


async def main():
    loop = asyncio.get_event_loop()
    
    # Run blocking operation in thread pool
    result = await loop.run_in_executor(
        ThreadPoolExecutor(),
        blocking_io_operation
    )
    
    print(f"Result: {result}")


# For CPU-bound work in async
def cpu_work():
    return sum(i * i for i in range(10**7))


async def main_with_cpu():
    loop = asyncio.get_event_loop()
    
    # Run CPU work in process pool
    result = await loop.run_in_executor(
        ProcessPoolExecutor(),
        cpu_work
    )
    
    print(f"CPU result: {result}")

Best Practices

Practice Implementation
Use async consistently Don’t mix sync/async
Avoid blocking calls Use async libraries
Handle errors properly Use try/except in await
Limit concurrency Use semaphores
Set timeouts Prevent hangs
Use gather for parallelism Concurrent execution

Common Pitfalls

# โŒ BAD: Blocking the event loop
async def bad_example():
    import time
    time.sleep(1)  # Blocks entire event loop!
    return "Done"


# โœ… GOOD: Using async sleep
async def good_example():
    await asyncio.sleep(1)  # Non-blocking
    return "Done"


# โŒ BAD: Not awaiting
async def bad_await():
    fetch_data("http://example.com")  # Forgot await!


# โœ… GOOD: Proper await
async def good_await():
    await fetch_data("http://example.com")


# โŒ BAD: Sequential when concurrent possible
async def bad_sequential():
    r1 = await fetch("url1")
    r2 = await fetch("url2")
    r3 = await fetch("url3")


# โœ… GOOD: Concurrent with gather
async def good_concurrent():
    r1, r2, r3 = await asyncio.gather(
        fetch("url1"),
        fetch("url2"),
        fetch("url3")
    )

Performance Comparison

import asyncio
import time


async def measure_performance():
    urls = [f"http://api.example.com/item/{i}" for i in range(100)]
    
    # Sequential
    start = time.time()
    for url in urls:
        await fetch_data(url, 0.01)
    sequential_time = time.time() - start
    
    # Concurrent
    start = time.time()
    await asyncio.gather(*[fetch_data(url, 0.01) for url in urls])
    concurrent_time = time.time() - start
    
    print(f"Sequential: {sequential_time:.2f}s")
    print(f"Concurrent: {concurrent_time:.2f}s")
    print(f"Speedup: {sequential_time / concurrent_time:.1f}x")

Conclusion

Asynchronous programming is essential for building high-performance applications, especially I/O-bound systems. By understanding the difference between concurrency and parallelism and applying the patterns in this guide, you can dramatically improve your application’s performance.

Key takeaways:

  1. Concurrency vs parallelism - Concurrency handles multiple tasks, parallelism runs them simultaneously
  2. Use async for I/O - Network requests, file operations, databases
  3. Use processes for CPU - Computation-intensive tasks
  4. Avoid blocking - Never use sync operations in async code
  5. Use gather - Run independent async tasks concurrently
  6. Handle errors - Use try/except and return_exceptions

By mastering these async patterns, you’ll build faster, more responsive applications.

Resources

Comments