3.1 The asyncio
Module
Nobody's been creating their own threads for async tasks for a while now. Well, you can create them, but such actions are considered too low-level and are used only by framework developers. And even then, only when absolutely necessary.
Nowadays, it's all about
asynchronous programming, async/await
operators
and coroutines with tasks. Let's break it down...
A Bit of History
Initially, Python used coroutines based on generators to tackle async programming challenges.
Then, in Python 3.4, the asyncio
module came along
(sometimes called async IO
), implementing async programming mechanisms.
In Python 3.5, the async/await
construct was introduced.
Now for a bit of background info. First, I'll briefly cover all these things, then in more detail, and then even more detailed. This is necessary because they almost all work together, and it's impossible to explain one in detail without referencing the others.
The asyncio
Module
The asyncio
module is designed for writing
asynchronous programs, enabling tasks to run
concurrently. It supports async I/O operations,
timers, sockets, coroutine execution, and multithreading, working within
one or multiple event loops.
Coroutines
Coroutines are asynchronous functions defined using
the async def
keyword. Coroutines allow you to
pause their execution using the await keyword
, which
allows other coroutines to run during that time
.
import asyncio
# Define an asynchronous function (coroutine)
async def main():
print('Hello ...')
# Pause execution for 1 second
await asyncio.sleep(1)
print('... World!')
# Run the main() coroutine in the event loop
asyncio.run(main())
Event Loop
The event loop controls the execution of coroutines, tasks, and other
async operations. Calling asyncio.run()
starts the event loop and
runs the coroutine until completion.
import asyncio
async def main():
print('Hello ...')
await asyncio.sleep(1)
print('... World!')
# Get the current event loop
loop = asyncio.get_event_loop()
# Run the coroutine until complete
loop.run_until_complete(main())
# Close the event loop after all tasks are done
loop.close()
Tasks
Tasks allow running coroutines concurrently. They're created using
asyncio.create_task()
or
asyncio.ensure_future()
.
import asyncio
# Define a coroutine that will run with a delay
async def say_after(delay, what):
# Pause execution for the given time
await asyncio.sleep(delay)
print(what)
# Main coroutine
async def main():
# Create tasks to run coroutines concurrently
task1 = asyncio.create_task(say_after(1, 'hello'))
task2 = asyncio.create_task(say_after(2, 'world'))
# Wait for both tasks to finish
await task1
await task2
# Run the main coroutine
asyncio.run(main())
Futures
Future
objects represent results of async operations
that will be available in the future. For instance, they're used to
wait for an async task to finish.
import asyncio
# Define a coroutine that simulates a long task
async def long_running_task():
print('Task started')
# Pause execution for 3 seconds
await asyncio.sleep(3)
print('Task finished')
return 'Result'
# Main coroutine
async def main():
# Create a future to await the task completion
future = asyncio.ensure_future(long_running_task())
# Wait for the task to finish and get the result
result = await future
print(f'Task result: {result}')
# Run the main coroutine
asyncio.run(main())
3.2 Asynchronous Function — async def
An asynchronous function is declared just like a regular one, except
you write async
before the def
keyword.
async def FunctionName(parameters):
function code
An asynchronous function is declared like a regular one, called like a regular one, but it returns something different. If you call an asynchronous function, it won't return the result immediately but a special object — a coroutine.
You can even check this:
import asyncio
async def main():
print("Hello World")
# Call an asynchronous function, which returns a coroutine
result = main()
# Check the type of the result
print(type(result)) # <class 'coroutine'>
So, what's happening? When you mark a function with async, you're effectively adding a decorator that does something like this:
def async_decorator(func):
# Create a Task object
task = Task()
# Pass your function func to it for execution
task.target = func
# Add task object to the event loop's task queue
eventloop.add_task(task)
# Return the task object
return task
And your code becomes like:
import asyncio
@async_decorator
def main():
print("Hello World")
result = main()
print(type(result)) # <class 'coroutine'>
Here's the point of this analogy:
When you call an asynchronous function, a special Task
object is created, which will execute your
function, but sometime in the future. Maybe in 0.0001 sec, or maybe in 10.
This task
object is returned to you right away as the result of calling your asynchronous function. This is
the
coroutine.
Your asynchronous function might not have even started executing yet, but you already have the task
(coroutine) object.
Why do you need this task
(coroutine)? You may not be able to do much with it, but there are 3 things you can do:
- Wait for the asynchronous function to complete.
- Wait for the asynchronous function to finish and get the result from the coroutine.
- Wait for 10 (or any number) asynchronous functions to complete.
I'll explain how to do that below.
3.3 The await
Operator
Most actions with a coroutine start with "waiting for an asynchronous function to complete." That's why we have a special await
operator for this action.
You just need to write it before the coroutine:
await coroutine
Or right before calling an asynchronous function:
await async_function(arguments)
When Python encounters the await
operator in the code, it pauses the current function's execution
and waits until
the coroutine completes — until the asynchronous function referenced by the coroutine finishes.
Important!
The await
operator is used only inside an asynchronous function to pause execution until
another coroutine or asynchronous operation completes.
This is done to simplify switching between calls to asynchronous functions. Such an await
call is essentially a declaration "we'll be waiting here for an unknown amount of time – go ahead and run other async
functions."
Example:
import asyncio
# Define an asynchronous function
async def async_print(text):
print(text)
# Main asynchronous function
async def main():
# Use await to wait for the async function to finish
await async_print("Hello World")
# Run the main event loop and execute the main() coroutine
asyncio.run(main()) # runs the async function
Actually, the await
operator is even smarter — it also returns the result of the asynchronous function
it was called on.
Example:
import asyncio
# Define an asynchronous function that adds two numbers
async def async_add(a, b):
return a + b
# Main asynchronous function
async def main():
# Use await to get the result of async_add
sum = await async_add(100, 200)
print(sum)
# Run the main event loop and execute the main() coroutine
asyncio.run(main()) # runs the async function
So, to sum up, the await
operator:
- Pauses the current async function until another coroutine or async operation finishes.
- Returns the result of the async operation or coroutine.
- Can be used only within an async function.
GO TO FULL VERSION