Asynchronous programming can significantly boost your Python application’s performance, especially when dealing with I/O-bound operations like network requests or file processing. While it might sound complex, understanding the core concepts of async and await can be surprisingly straightforward. This post will break down the simplest possible async/await example in Python, demonstrating how these keywords work together to unlock concurrent execution without the headaches of threading or multiprocessing. We’ll explore practical use cases, common pitfalls, and best practices, empowering you to write efficient and responsive Python code.
What are async and await?
async and await are keywords introduced in Python 3.5 to simplify asynchronous programming. async defines a coroutine, a special type of function that can pause execution at specific points and resume later. await is used inside an async function to pause execution until a particular awaitable object (like a coroutine or a task) completes. This allows other tasks to run while the current one is waiting, maximizing efficiency.
Imagine a chef preparing multiple dishes. Instead of waiting for each dish to finish cooking before starting the next, they can start one, move on to the next while the first is cooking, and then come back to the first when it’s ready. This is analogous to how async/await works, enabling concurrent execution within a single thread.
This approach avoids the overhead and complexities associated with threads or processes, making it a more lightweight and efficient way to achieve concurrency, especially in I/O-bound scenarios.
The Simplest Example
Here’s the simplest async/await example you’ll likely find:
import asyncio async def my_coroutine(): print("Coroutine started") await asyncio.sleep(1) Pause for 1 second print("Coroutine finished") async def main(): await my_coroutine() asyncio.run(main())
Let’s dissect it. my_coroutine() is our coroutine, marked with async. Inside, await asyncio.sleep(1) simulates a 1-second I/O operation. main(), also a coroutine, calls my_coroutine() using await, which pauses main() until my_coroutine() completes. Finally, asyncio.run(main()) starts the event loop and executes our code.
Real-World Applications
While the basic example demonstrates the core mechanics, the true power of async/await shines in real-world applications. Consider making multiple web requests concurrently:
import aiohttp import asyncio async def fetch_url(session, url): async with session.get(url) as response: return await response.text() async def main(): async with aiohttp.ClientSession() as session: tasks = [fetch_url(session, "https://example.com"), fetch_url(session, "https://google.com")] results = await asyncio.gather(tasks) print(results) asyncio.run(main())
Here, fetch_url fetches content from a URL using aiohttp, an asynchronous HTTP client. main() creates multiple tasks using asyncio.gather and awaits their completion, effectively fetching multiple URLs concurrently.
- Improved Responsiveness: Asynchronous operations prevent your application from blocking while waiting for I/O.
- Enhanced Performance: Concurrent execution significantly reduces overall processing time, especially with multiple I/O operations.
Common Pitfalls and Best Practices
A common mistake is calling an async function without await inside another async function. This doesn’t execute the coroutine; it just creates a coroutine object. Ensure you always use await when calling coroutines within other coroutines. Also, be mindful of blocking operations within coroutines. Using synchronous libraries within async functions can negate the benefits of asynchronous programming.
- Always use
awaitwhen calling coroutines. - Avoid blocking calls within coroutines.
- Use asynchronous libraries whenever possible.
Leveraging the power of asynchronous operations correctly can result in significant performance gains. According to a recent study, asynchronous programming can increase web server throughput by up to 70% in I/O-bound scenarios.
This approach is especially relevant when dealing with:
- Web scraping
- API interactions
Frequently Asked Questions
Q: What’s the difference between async/await and threading?
A: async/await achieves concurrency within a single thread, while threading uses multiple threads. async/await is generally more efficient for I/O-bound tasks, while threading can be beneficial for CPU-bound tasks.
Asynchronous programming with async and await is a powerful tool in the Python developer’s arsenal. By understanding the core concepts and following best practices, you can write efficient, responsive, and highly concurrent applications. Start experimenting with async/await in your own projects and unlock the potential of concurrent programming in Python. Visit this official Python documentation for in-depth information and more advanced scenarios. You might also find this Real Python tutorial helpful for further learning. Explore how you can integrate these techniques to optimize your data processing pipelines or enhance your web application’s performance. Check out this article for a deeper dive into Advanced Python Concurrency as well.
[Infographic about async/await vs. threading]
Question & Answer :
I’ve read many examples, blog posts, questions/answers about asyncio / async / await in Python 3.5+, many were complex, the simplest I found was probably this one.
Still it uses ensure_future, and for learning purposes about asynchronous programming in Python, I would like to see an even more minimal example, and what are the minimal tools necessary to do a basic async / await example.
Question: is it possible to give a simple example showing how async / await works, by using only these two keywords + code to run the async loop + other Python code but no other asyncio functions?
Example: something like this:
import asyncio async def async_foo(): print("async_foo started") await asyncio.sleep(5) print("async_foo done") async def main(): asyncio.ensure_future(async_foo()) # fire and forget async_foo() print('Do some actions 1') await asyncio.sleep(5) print('Do some actions 2') loop = asyncio.get_event_loop() loop.run_until_complete(main())
but without ensure_future, and still demonstrates how await / async works.
To answer your questions, I will provide three different solutions to the same problem.
Case 1: just normal Python
import time def sleep(): print(f'Time: {time.time() - start:.2f}') time.sleep(1) def sum(name, numbers): total = 0 for number in numbers: print(f'Task {name}: Computing {total}+{number}') sleep() total += number print(f'Task {name}: Sum = {total}\n') start = time.time() tasks = [ sum("A", [1, 2]), sum("B", [1, 2, 3]), ] end = time.time() print(f'Time: {end-start:.2f} sec')
Output:
Task A: Computing 0+1 Time: 0.00 Task A: Computing 1+2 Time: 1.00 Task A: Sum = 3 Task B: Computing 0+1 Time: 2.00 Task B: Computing 1+2 Time: 3.00 Task B: Computing 3+3 Time: 4.00 Task B: Sum = 6 Time: 5.00 sec
Case 2: async/await done wrong
import asyncio import time async def sleep(): print(f'Time: {time.time() - start:.2f}') time.sleep(1) async def sum(name, numbers): total = 0 for number in numbers: print(f'Task {name}: Computing {total}+{number}') await sleep() total += number print(f'Task {name}: Sum = {total}\n') start = time.time() loop = asyncio.new_event_loop() tasks = [ loop.create_task(sum("A", [1, 2])), loop.create_task(sum("B", [1, 2, 3])), ] loop.run_until_complete(asyncio.wait(tasks)) loop.close() end = time.time() print(f'Time: {end-start:.2f} sec')
Output:
Task A: Computing 0+1 Time: 0.00 Task A: Computing 1+2 Time: 1.00 Task A: Sum = 3 Task B: Computing 0+1 Time: 2.00 Task B: Computing 1+2 Time: 3.00 Task B: Computing 3+3 Time: 4.00 Task B: Sum = 6 Time: 5.00 sec
Case 3: async/await done right
The same as case 2, except the sleep function:
async def sleep(): print(f'Time: {time.time() - start:.2f}') await asyncio.sleep(1)
Output:
Task A: Computing 0+1 Time: 0.00 Task B: Computing 0+1 Time: 0.00 Task A: Computing 1+2 Time: 1.01 Task B: Computing 1+2 Time: 1.01 Task A: Sum = 3 Task B: Computing 3+3 Time: 2.01 Task B: Sum = 6 Time: 3.02 sec
Case 1 and case 2 give the same 5 seconds, whereas case 3 just 3 seconds. So the async/await done right is faster.
The reason for the difference is within the implementation of the sleep function.
# Case 1 def sleep(): ... time.sleep(1) # Case 2 async def sleep(): ... time.sleep(1) # Case 3 async def sleep(): ... await asyncio.sleep(1)
In case 1 and case 2, they are the “same”: they “sleep” without allowing others to use the resources. Whereas in case 3, it allows access to the resources when it is asleep.
In case 2, we added async to the normal function. However the event loop will run it without interruption. Why? Because we didn’t say where the loop is allowed to interrupt your function to run another task.
In case 3, we told the event loop exactly where to interrupt the function to run another task. Where exactly? Right here!
await asyncio.sleep(1)
For more on this, read here.
Consider reading