Concurrency in Python: Understanding Asyncio vs Synchronous Code

Mar 17, 2024 ยท 6 min read

Python is often used for building complex applications that need to handle multiple tasks concurrently. There are two main approaches in Python for writing concurrent code:

  1. Asyncio - An asynchronous, non-blocking approach based on an event loop.
  2. Synchronous - A sequential, blocking approach where each line of code finishes before moving to the next.

Understanding the difference between these approaches is key to writing efficient, scalable Python programs. In this comprehensive guide, we'll cover:

  • The fundamentals of concurrency and blocking vs non-blocking code
  • How asyncio works and why it can improve performance
  • When to use asyncio vs synchronous Python
  • Practical examples and code walkthroughs
  • Whether you're new to concurrency in Python or looking to deepen your knowledge, read on to master the critical skill of managing competing tasks in your Python apps.

    Blocking vs Non-Blocking Code

    The key difference between synchronous and asyncio Python lies in how they handle blocking operations.

    In programming, a block operation pauses the execution of code until some other process finishes. Common examples include:

  • Waiting for user input
  • Reading/writing files
  • Making network requests
  • Accessing databases
  • Blocking code stops all other work in your program until the blocking task completes. This can bottleneck performance.

    Non-blocking code uses async programming to avoid pauses. Rather than waiting for blocking tasks to finish, the program can switch to other work while awaiting the results.

    Asyncio provides an event loop and async/await syntax so you can write non-blocking concurrent Python code easily.

    Let's look at a simple example of blocking vs non-blocking code:

    # Synchronous blocking code
    import time 
    
    start = time.time()
    print("Starting")  
    
    time.sleep(2) # Pause for 2 seconds (blocking)
    
    print(f"Finished in {time.time() - start} seconds")

    This synchronous code blocks/pauses for 2 seconds during time.sleep().

    Now let's make it non-blocking with asyncio:

    import asyncio
    
    async def sleep_task():
        print("Sleep task starting")
        await asyncio.sleep(2)  
        print("Sleep task done")
    
    async def main():
        start = time.time()
        print("Program starting")
        
        await sleep_task()
        
        print(f"Program finished in {time.time() - start} seconds")  
    
    asyncio.run(main())

    By using asyncio.sleep() and await, we avoid blocking the main thread. The program can now do other work while awaiting the sleep task's result.

    This example shows the performance difference - asyncio allows concurrent execution where synchronous code would block.

    How Asyncio Works in Python

    The asyncio module lets you write asynchronous, non-blocking programs using an event loop:

  • The event loop schedules and manages tasks concurrently.
  • Coroutines define async tasks using async/await syntax.
  • The loop awaits coroutines and executes callbacks when they are ready.
  • Here is a simplified overview of running asyncio code:

    1. All async tasks get wrapped in coroutines and submitted to the event loop queue.
    2. The event loop begins executing tasks and awaits any I/O bound ops like network requests.
    3. Rather than blocking, the loop switches to other ready tasks while awaiting I/O results.
    4. Callbacks execute when the awaited I/O finishes to handle results and continue execution.
    5. The loop manages concurrency by coordinating between tasks and callbacks.

    Within this architecture:

  • Asyncio schedules cooperative multitasking - Each coroutine yields control back to the event loop when awaiting I/O, allowing other coroutines to run.
  • The loop interleaves execution of coroutines by awaiting them in turns. This allows concurrent progress.
  • Let's see an asyncio example to illustrate the components working together:

    import asyncio
    import time
    
    async def fetch_data():
        print("Starting fetch")
        await asyncio.sleep(2) # Pause without blocking  
        print("Completed fetch - got data!")
        return {"data": 1}
    
    async def process(data):
        print(f"Processing {data}")
        await asyncio.sleep(3)
        print("Finished processing")
    
    async def main():
        start = time.time()  
        print("Program begin")
        
        data = await fetch_data() # Await coroutine
        await process(data)  
        
        print(f"Program complete in {time.time() - start} seconds")
    
    asyncio.run(main())

    Output:

    Program begin
    Starting fetch
    Completed fetch - got data!
    Processing {'data': 1} 
    Finished processing
    Program complete in 3.00 seconds

    This shows how:

  • main orchestrates coroutine execution
  • fetch_data and process run concurrently, enabling better performance
  • Awaiting coroutines allows non-blocking coordinated multitasking
  • Understanding asyncio architecture helps write efficient async programs.

    When to Use Asyncio vs Synchronous Python

    So when should you use asynchronous asyncio, and when is synchronous code fine?

    Use asyncio for I/O bound workloads - Network requests, reading files, user input all benefit from non-blocking async execution.

    Use synchronous code for CPU heavy processing - Math operations, data processing, algorithms should run sequentially.

    Some guidelines on when to use each approach:

    Asyncio is great when:

  • Making concurrent network calls (HTTP requests, websockets)
  • Reading/writing multiple files or streams
  • Waiting for user input or other I/O
  • Calling APIs in parallel
  • Database access (with async db drivers)
  • Coordinating queue/task distribution
  • Synchronous code works well for:

  • Math heavy data analysis algorithms
  • CPU intensive image/video processing
  • Sorting/processing internal data structures
  • Executing sequential business logic
  • Calling a few APIs serially
  • For many programs you'll want a mix of asynchronous I/O coordination with synchronous computational work.

    Asyncio in Practice

    Let's walk through some practical async coding patterns to see how asyncio can create responsive concurrent programs.

    We'll build an async web scraper that fetches multiple URLs concurrently:

    import asyncio
    import aiohttp
    
    async def fetch(url):
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                data = await response.text()
                return data
                
    async def main():
        urls = ["https://example.com", "https://python.org"] 
        
        tasks = []
        for url in urls:
            tasks.append(asyncio.create_task(fetch(url)))
            
        results = await asyncio.gather(*tasks)
        
        for result in results:
            print(f"Got {len(result)} bytes from url")
     
    asyncio.run(main())

    Key points:

  • aiohttp handles async network requests without blocking
  • create_task schedules coroutines concurrently
  • gather awaits multiple tasks at once
  • We fetch both URLs in parallel, improving speed
  • Other common async patterns include:

  • Using asyncio.Queue for producer/consumer task distribution
  • Running periodic background tasks with loop.create_task()
  • Coordinating async generators to stream data
  • Capping concurrency with asyncio.Semaphore
  • Practice with these patterns unlocks the true power of asyncio in Python.

    Summary

    Here are the key takeaways:

  • Asyncio provides an event loop to run non-blocking concurrent tasks using async/await
  • Coroutines yield control instead of blocking, enabling cooperative multitasking
  • Use asyncio for I/O workloads, synchronous code for CPU processing
  • Asyncio suits parallel network/file/data ops, sync fits sequential algorithms
  • Asyncio patterns like queues, semaphores, generators enable scalable apps
  • Hopefully this overview gives you a comprehensive foundation on using asyncio effectively in Python. Mastering async programming unlocks huge performance potential.

    The event loop handles much complexity - our code can focus on application logic while asyncio schedules efficient task execution under the hood.

    To dive deeper into concurrency with Python, check out these additional resources:

  • Asyncio documentation - Official API reference
  • asyncio Queue guide - Async task distribution
  • Asynchronous Python book - In-depth async coding patterns
  • Browse by tags:

    Browse by language:

    The easiest way to do Web Scraping

    Get HTML from any page with a simple API call. We handle proxy rotation, browser identities, automatic retries, CAPTCHAs, JavaScript rendering, etc automatically for you


    Try ProxiesAPI for free

    curl "http://api.proxiesapi.com/?key=API_KEY&url=https://example.com"

    <!doctype html>
    <html>
    <head>
        <title>Example Domain</title>
        <meta charset="utf-8" />
        <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
    ...

    X

    Don't leave just yet!

    Enter your email below to claim your free API key: