What is Asynchronous? Asynchronous implies that something is happening not at the same time or simultaneously.

A definition of the same from The Art of Concurrency book states:

“Separate execution streams that can run concurrently in any order relative to each other are asynchronous.”

For example, “Asynchronous Function Call” implies that the function has been called and while it executes, you can carry on with other tasks. Later on you can see whether the function has returned something or not.

What is Future? Future allows you to get the results from an asynchronous function call. Meaning, while we made an asynchronous function call, we went on to execute the remaining tasks, and later on we want to check or get the results of that particular function. In essence, Future is a place that you reserve for a value that will be generated in the future!

The above diagram shows what an asynchronous task comprises of.

Example scenario:

  • Consider you have task1, task2 and task3.
  • task2 is a complicated and long-running task.
  • task1 and task3 are not dependent on task2.
  • So instead for waiting for task2 to be completed, and then moving on to the task3, we can let task2 be executed asynchronously and meanwhile complete all other tasks.
  • When task2 is done, we will be notified of it and we can retrieve the results.

The real world application for this is primarily in non-blocking I/O operations. Where reading and writing operations are done asynchronously. This is where the term Asyncio comes from probably, where non-blocking IO operations are implemented using asynchronous programming.

“This lets us handle multiple I/O operations at once, while still allowing our application to remain responsive.” - Python Concurrency with asyncio

Thus, Asyncio enables us to do asynchronous programming in python using coroutines. What is a coroutine? It is function that can be called, suspended at any point and resumed at any time. Let’s define a coroutine function

import asyncio
async def my_coroutine():
	#-
	#-
	#-

What async in the above example does is, it wraps the function my_coroutine(). and when you call the function, it actually returns an object which is asynchronous, and can be used.

Let’s try to call a async function like we would a regular function:

#importing the library
import asyncio

#defining a coroutine
async def print_something():
	print("Hey Ash!")

#calling the function
print_something()

Trying to run the above block of code will produce the following error.

RuntimeWarning: coroutine 'print_something' was never awaited

In order to execute the coroutine we use await keyword. Remember, the keyword await must always be written inside a async function and never outside it, or else you will get an error.

Let’s try again, this time using asyncio.run()

#importing the library
import asyncio

#defining a coroutine
async def print_something():
	print("Hey Ash!")

#calling the function
asyncio.run(print_something())

Here, asyncio created an event loop, that enables us to execute our coroutine.

Hey Ash!

Now we will write a better code to understand await keyword.

#import asyncio library
import asyncio

#create an async function 
async def task1():
	print("Hey Ash")
	await task2(10) #wait will the task2 is fully executed
	print("Hello World")

#create an asunc function 
async def task2(num):
	for i in range(num):
	print(i)
	await asyncio.sleep(1) #wait for 1 second before printing the next i

#run the coroutine task1
asyncio.run(task1())

In the above example the following steps are happening:

  • First we import the library of asyncio.
  • We write a async function task1 to print “Hey Ash”, then called another async function task2 and asked the task1 to wait until task2 has finished its execution. This is done by using the await keyword. After this task1 has one more line left to execute, which is “Hello World”.
  • The task2 is to print numbers in a range. After printing each number, we used an await keyword to make the process sleep for 1 second and then move on to print another number.
  • At last, we run the task1 using asyncio.run(task1)
Hey Ash
0
1
2
3
4
5
6
7
8
9
Hello World
  • As you can see when we call the task1, it prints “Hey Ash” and then the next line is to wait for the complete execution of task2.
  • Thus, the numbers 1 to 9 are printed and then it returns back to the last line in task1 which is to print “Hello World”.

Now what will be the output for the following block of code? Run it yourself.

#import asyncio library
import asyncio

#create an async function 
async def task1():
	await task2(5) #wait will the task2 is fully executed
	print("Hello World")

#create an asunc function 
async def task2(num):
	for i in range(num):
	print(i)
	await asyncio.sleep(1) #wait for 1 second before printing the next i

#run the coroutine task1
asyncio.run(task1())

The result of the above code is as follows:

0
1
2
3
4
Hello World

The task1 could only execute the print statement of “Hello World” after waiting for the task2 to be executed completely.

Now, to disappoint you a little bit, that is, if you haven’t noticed it yet, all the above code weren’t exactly asynchronous. Because, you made a task wait while the other task was working. This is not not we are aiming for right? This happened because we haven’t yet created an asynchronous task.

So let’s create one now.

#import asyncio library
import asyncio

#create an async function 
async def task1():
	task = asyncio.create_task(task2(5)) #Create our 1st asynchronous task!
	print("Hello World")

#create an asunc function 
async def task2(num):
	for i in range(num):
	print(i)
	await asyncio.sleep(1) #wait for 1 second before printing the next i

#run the coroutine task1
asyncio.run(task1())

In the above code, within the task1, we created a asynchronous task using asyncio.create_task(). I just realized that you might be confused between task1, task2, and task. That’s my bad. task1 and task2 are async functions and NOT asynchronous tasks. But task is an asynchronous task. Hmm, so much for writing a blog.

Now, what we did by creating a task is that, we are asking python, to let the function task1 execute it’s “Hello World” command while task2 function is busy executing printing numbers. Thus no one’s waiting around for anybody in here.

The result of the above code is as follows:

Hello World
0

You might wonder, what happened to the numbers 1, 2, 3, and 4? Before they could be printed, the task1 terminated. To explain in simple way, consider A and B. A is supposed to to do some task and is also meant to call B to do some task as well. A’s task is a small one, but B has a lot to do. While B was completing his chores, A completed his and did not bother to wait for B to finish his. This ended the main process, since it was A who called B in the first place.

To avoid this, we add a line towards the end of the task1 function, so that task1 waits after it has completed executing all its code, so that task2 can execute everything as well. We add await as follows.

#import asyncio library
import asyncio

#create an async function 
async def task1():
	task = asyncio.create_task(task2(5)) #Create our 1st asynchronous task!
	print("Hello World")
	await task

#create an asunc function 
async def task2(num):
	for i in range(num):
	print(i)
	await asyncio.sleep(1) #wait for 1 second before printing the next i

#run the coroutine task1
asyncio.run(task1())

The result of the above code will be as follows:

Hello World
0
1
2
3
4

Now For our last example, run the following code and try to understand what is happening.

#importing the library
import asyncio


async def print_something():

	print("Hey Ash!")
	task = asyncio.create_task(print_numbers(10))
	await asyncio.sleep(5)
	print("Finish")
	await task

async def print_numbers(num):

	for i in range(num):
	print(i)
	await asyncio.sleep(1)

asyncio.run(print_something())

The result is as follows:

Hey Ash!
0
1
2
3
4
Finish
5
6
7
8
9

In the above example, the print_something function had 3 jobs, to print “Hey Ash!”, then call the function print_numbers, and at last to print “Finish”. While the print numbers was executing, it printed out “Hey Ash!” and then after 5 seconds printed out “Finish”. It did not wait for print_numbers to complete the executing. This is a very basic example to show the power of asynchronous programming.

I hope the blog was able to shed some insight into asynchronous programming for you to start with. Follow up on the next blog to dive in deeper.