Asynchronous Programming with asyncio
Introduction
Asynchronous programming is a type of parallel programming in which a unit of work runs separately from the main application thread and notifies the calling thread of its completion, failure, or progress. You might have heard about it, as it's becoming more popular due to its ability to increase the overall efficiency of your application. Python introduced support for asynchronous I/O in version 3.4 through the asyncio
module.
This tutorial will guide you through understanding and applying asynchronous programming in Python with asyncio. We'll start with the basics and gradually move towards more advanced topics.
Prerequisites
Before we start, make sure you have a basic understanding of Python programming. Also, you need to have Python 3.7 or higher installed on your machine.
What is asyncio?
asyncio
is a library used for writing single-threaded concurrent code using coroutines, multiplexing I/O access over sockets and other resources, running network clients and servers, and other related primitives.
Coroutines with asyncio
A coroutine is a generalized version of a subroutine that can be exited and later re-entered at multiple different points. Let's look at a simple coroutine that performs a countdown.
import asyncio
async def countdown(number):
while number > 0:
print('Countdown: ', number)
number -= 1
await asyncio.sleep(1)
asyncio.run(countdown(10))
In the example above, the async def
statement is used to define a coroutine. Inside the coroutine, the await
statement is used to pause the execution until the operation is done, then the control is returned back to the event loop.
Tasks with asyncio
Tasks are used to schedule coroutines concurrently. When a coroutine is wrapped into a Task with functions like asyncio.create_task()
the coroutine is automatically scheduled to run soon.
import asyncio
async def task(name, times):
for i in range(times):
print(f'{name} {i}')
await asyncio.sleep(1)
async def main():
# Schedule three calls *concurrently*:
await asyncio.gather(
task('A', 3),
task('B', 3),
task('C', 3),
)
asyncio.run(main())
In the example above, asyncio.gather()
is used to run coroutines concurrently as asyncio tasks.
asyncio and blocking I/O
In asyncio, blocking I/O operations should be offloaded to a separate thread to avoid blocking the event loop. The run_in_executor
method can be used to run a function in a separate thread and return a Future representing the eventual result.
import asyncio
import urllib.request
async def download(url):
loop = asyncio.get_event_loop()
data = await loop.run_in_executor(None, urllib.request.urlopen, url)
return data.read()
async def main():
data = await download('https://www.python.org')
print(len(data))
asyncio.run(main())
In the example above, loop.run_in_executor()
is used to run urllib.request.urlopen()
in a separate thread.
Conclusion
Asynchronous programming is a powerful tool for creating efficient and responsive applications, and asyncio is a great library to handle asynchronous I/O in Python. We've only scratched the surface of what you can do with asyncio. There are many other features like event loops, futures, and protocols which you can explore in the official Python documentation.
Remember, the key to understanding asyncio is practice. So, try to write your own asynchronous programs and see how it improves the performance of your applications.