Making Python Asynchronous: An Introduction to asyncio

Mar 17, 2024 ยท 5 min read

Asynchronous programming has become increasingly important as applications grow more complex and users expect lower latency. Python's asyncio module introduces asynchronous capabilities to Python, allowing you to write code that efficiently handles concurrent operations and long-running tasks.

In this article, we'll cover the basics of asyncio and asynchronous programming. We'll look at common use cases for asyncio and examples of how it can make Python code more performant.

What is Asynchronous Programming?

Asynchronous programming refers to code that can initiate long-running actions without blocking further execution until those actions finish. Instead of waiting for each operation to complete before starting the next one, asynchronous programs can execute multiple operations concurrently.

For example, a synchronous program might do:

start = time.time()
download_page(url1) # blocks
download_page(url2) # waits for url1 to finish
download_page(url3) 
end = time.time()

An asynchronous version could do:

start = time.time() 
download_page_task1 = asyncio.create_task(download_page(url1))
download_page_task2 = asyncio.create_task(download_page(url2)) 
download_page_task3 = asyncio.create_task(download_page(url3))
await download_page_task1
await download_page_task2
await download_page_task3
end = time.time()  

By not blocking, the total time is reduced. Asynchronous programming enables concurrent execution which improves speed and efficiency.

Introducing Python's asyncio

The asyncio module was introduced in Python 3.4 to provide native asynchronous programming capabilities. It uses cooperative multitasking and an event loop to manage multiple tasks efficiently.

Some key concepts in asyncio:

  • Coroutines: Special functions that can pause execution without blocking. Defined with async def
  • Tasks: Wrappers around coroutines that run them on the event loop
  • Event Loop: Schedules and runs tasks and callbacks
  • Awaitable: Objects that can be awaited using await - usually Tasks and Futures
  • When asyncio code runs, it uses a single-threaded event loop to execute all the Tasks that are created. By integrating cooperative multitasking natively into Python, asyncio makes it much easier to write asynchronous code than previous approaches.

    Common Use Cases for asyncio

    Here are some examples of where asyncio can be useful:

    Network Programming

    Fetching multiple web pages or handling many client connections concurrently. Asyncio shines for high concurrency network tasks.

    Database Access

    Performing multiple queries with an async database driver. Overlapping DB I/O instead of waiting for each query to finish.

    File Processing

    Reading/writing multiple files at once or doing CPU intensive data processing concurrently.

    Task Queues / Job Scheduling

    Managing queues of work and distributing jobs across threads and processes.

    Real-time Services

    Building responsive services where latency matters - chat systems, live metrics, etc.

    Asyncio works great for I/O bound and high-concurrency workloads. It's less helpful for purely CPU heavy processes since it uses a single thread.

    Asyncio in Action

    To demonstrate asyncio, let's walk through an example program that fetches multiple web pages concurrently:

    import asyncio
    import time
    
    async def fetch_page(url):
        print(f"Fetching {url}")
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                html = await response.text()
                return html
    
    async def main():
        
        urls = ['https://www.jython.org',
                'http://olympus.realpython.org/',
                'https://realpython.com/']
        
        tasks = []
        
        start = time.time()
        
        for url in urls:
            tasks.append(asyncio.create_task(fetch_page(url)))
            
        results = await asyncio.gather(*tasks)
        
        end = time.time()
        
        print(f"Total time: {end-start}")
        
    asyncio.run(main())

    Breaking this example down:

  • fetch_page is an asynchronous function that uses async with and await to request a URL without blocking
  • main creates tasks for each URL and gathers the results
  • Instead of waiting for each request to finish, they run concurrently
  • asyncio.run handles running the event loop
  • Executing the requests asynchronously reduces the total time compared to synchronous requests.

    Going Deeper into Asyncio

    To use asyncio effectively, there are some key details to understand:

  • The event loop schedules all asynchronous tasks and callbacks
  • Use asyncio.create_task() to schedule coroutines
  • await yields control back to event loop
  • asyncio.gather() awaits multiple Tasks concurrently
  • Use async with for asynchronous context managers
  • Additionally, many libraries like aiohttp provide async versions of functions to prevent blocking behavior.

    There are also more advanced asyncio features like:

  • asyncio.Queue - asynchronous queue for producer/consumer or work queues
  • asyncio.Lock - asynchronous version of threading lock
  • asyncio.Event - for notifying tasks
  • asyncio.Semaphore - for limiting concurrency
  • Common Pitfalls

    As asyncio uses cooperative multitasking, it's important async code avoids blocking behavior. Some common pitfalls:

  • Calling blocking code like time.sleep() without running it in an executor. Use asyncio.to_thread or run_in_executor
  • Forgetting to await async functions or Task objects
  • Blocking the event loop thread with long CPU bound work rather than I/O
  • Using threads without proper synchronization mechanisms
  • Debugging async code can be tricky since exceptions may surface in unexpected places due to changing execution order.

    Conclusion

    Asyncio provides a straightforward framework for writing asynchronous Python code. By handling the event loop, Tasks, and other async primitives, it simplifies concurrent programming.

    The advantages of asyncio include:

  • Increased throughput & efficiency for high-concurrency workloads
  • Lower latency responses
  • Better utilization of resources for I/O or network bound code
  • It does take some adjusting to the asynchronous paradigm to avoid blocking the event loop or forgetting await. Overall asyncio marks a major evolution in concurrent Python code.

    We've covered the basics here - for more advanced details about unlocking the full potential of asyncio, check out Real Python's Asyncio Course.

    Some key takeaways:

  • Asyncio enables asynchronous concurrent programming in Python
  • Use asyncio for network, database, and file I/O workloads
  • The event loop manages async tasks allowing non-blocking execution
  • await, async with, asyncio.create_task() and asyncio.gather() are key concepts
  • Follow async best practices to avoid blocking the event loop
  • I hope this overview gives you a better understanding of asyncio in Python!

    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: