CodeGym /Java Course /Python SELF EN /Asynchronous Methods

Asynchronous Methods

Python SELF EN
Level 25 , Lesson 2
Available

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.
2
Task
Python SELF EN, level 25, lesson 2
Locked
Creating and Executing Asynchronous Functions
Creating and Executing Asynchronous Functions
2
Task
Python SELF EN, level 25, lesson 2
Locked
Executing several tasks in parallel
Executing several tasks in parallel
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION