You may have heard of the term "asynchronous" in programming. Sounds fancy, eh? But what is it all about? In this post, I'll introduce you to the basic concept of asynchronous programming in Python.
Here, you'll learn about:
- The difference between synchronous and asynchronous programming
- Routines and coroutines
- Use cases of asynchronous programming
- How to create a basic asynchronous program in Python
Synchronous vs Asynchronous Programming
There are two approaches to handling tasks in a program. In the synchronous approach, tasks are performed one at a time in the order that they are called in. Tasks are block-driven, meaning that each task must finish before the next one can begin. The synchronous approach leverages the use of routines, also known as functions. This is the common approach most of us, especially beginners, take.
The other approach is the asynchronous, where different tasks can start, process, and finish in overlapping periods of time. It works by using coroutines. A coroutine is a special kind of function that can be paused and resumed. It's able to do so because it can maintain its state between pauses. By using coroutines, programs can run tasks concurrently.
The asynchronous approach is commonly used for I/O operations, database queries, and HTTP requests. You basically apply async for tasks that needs to wait.
To better understand these two approaches, we will create a Python program to demonstrate synchronous and asynchronous programming.
A Basic Program in Python
We will start by creating a basic program that let us do the tasks named eat_food()
and watch_video()
. We'll execute both of these tasks when we run our program.
First we will import the built-in time
module to (1) simulate the amount of time each task consumed and (2) record the execution time it took to run both tasks.
import time # for getting execution time
Then, we define our functions.
def eat_food():
print('Start eat_food()')
time.sleep(5) # I eat for 5 mins.
print('End eat_food()')
return 'Finished eating'
def watch_video():
print('Start watch_video()')
time.sleep(8) # I watch an 8 min. video
print('End watch_video()')
return 'Finished watching'
Create a main()
function and execute both tasks inside.
def main():
start_time = time.time()
result_eat = eat_food()
result_watch = watch_video()
end_time = time.time()
execution_time = end_time - start_time
print(f'Result of eat_food(): {result_eat}')
print(f'Result of watch_video(): {result_watch}')
print(f'Total execution time: {execution_time:.2f} seconds')
if __name__ == '__main__':
main()
Run the program, and the output should look like this.
Start eat_food()
End eat_food()
Start watch_video()
End watch_video()
Result of eat_food(): Finished eating
Result of watch_video(): Finished watching
Total execution time: 13.00 seconds
Notice that it took 13 seconds to execute both tasks: five seconds for eat_food()
and eight seconds for watch_video()
. The task were ran sequentially. This is how synchronous programming works.
Source Code
import time # for getting execution time
def eat_food():
print('Start eat_food()')
time.sleep(5) # I eat for 5 mins.
print('End eat_food()')
return 'Finished eating'
def watch_video():
print('Start watch_video()')
time.sleep(8) # I watch an 8 min. video
print('End watch_video()')
return 'Finished watching'
def main():
start_time = time.time()
result_eat = eat_food()
result_watch = watch_video()
end_time = time.time()
execution_time = end_time - start_time
print(f'Result of eat_food(): {result_eat}')
print(f'Result of watch_video(): {result_watch}')
print(f'Total execution time: {execution_time:.2f} seconds')
if __name__ == '__main__':
main()
Asynchronous Program in Python
To demonstrate asynchronous programming in Python, we will apply some modification to the program we just made. To do this, we will use a built-in library called Asyncio.
import asyncio
import time # for getting execution time
To turn a function into a coroutine, add the async
keyword to the function.
async def eat_food():
print('Start eat_food()')
...
async def watch_video():
print('Start watch_video()')
...
In a coroutine, you can use the await
keyword to specify what part of the code it is safe to pause.
async def eat_food():
print('Start eat_food()')
await asyncio.sleep(5) # I eat for 5 mins.
print('End eat_food()')
return 'Finished eating'
async def watch_video():
print('Start watch_video()')
await asyncio.sleep(8) # I watch an 8 min. video
print('End watch_video()')
return 'Finished watching'
Note: You can only put await
in an "awaitable" command, typically these are also coroutines.
If you think that you can call coroutines the same way as you call normal functions, I also thought so. However, it is not how it works. Coroutines return coroutine objects when you call them as is.
Result of eat_food(): <coroutine object eat_food at 0x000001A73ED2D180>
Result of watch_video(): <coroutine object watch_video at 0x000001A73ED2D9C0>
Calling coroutines properly can be done individually or by batch.
Individually using asyncio.create_task()
def main():
task_eat = async.create_task(eat_food())
task_watch = async.create_task(watch_video())
By batch using asyncio.gather()
def main():
batch = asyncio.gather(eat_food(), watch_video())
To get the return values of coroutines, you need to await
them
Individually
def main():
task_eat = async.create_task(eat_food())
task_watch = async.create_task(watch_video())
result_eat = await task_eat # 'Finished eating'
result_watch = await task_watch # 'Finished watching'
By batch
def main():
batch = asyncio.gather(eat_food(), watch_video())
result_eat, result_watch = await batch # ('Finished eating', 'Finished watching')
Since we have used await
inside the main()
function,
async def main():
...
Note: any function that contains the await
keyword must be declared an async function
Here's the final look of our main()
function.
Individually
async def main():
start_time = time.time()
task_eat = async.create_task(eat_food())
task_watch = async.create_task(watch_video())
result_eat = await task_eat # 'Finished eating'
result_watch = await task_watch # 'Finished watching'
end_time = time.time()
execution_time = end_time - start_time
print(f'Result of eat_food(): {result_eat}')
print(f'Result of watch_video(): {result_watch}')
print(f'Total execution time: {execution_time:.2f} seconds')
By batch
async def main():
start_time = time.time()
batch = asyncio.gather(eat_food(), watch_video())
result_eat, result_watch = await batch # ('Finished eating', 'Finished watching')
end_time = time.time()
execution_time = end_time - start_time
print(f'Result of eat_food(): {result_eat}')
print(f'Result of watch_video(): {result_watch}')
print(f'Total execution time: {execution_time:.2f} seconds')
To run our program, we will use asyncio.run()
and pass main()
as an argument.
if __name__ == '__main__':
asyncio.run(main())
Start eat_food()
Start watch_video()
End eat_food()
End watch_video()
Result of eat_food(): Finished eating
Result of watch_video(): Finished watching
Total execution time: 8.00 seconds
In the asynchronous approach, we managed to save five seconds in our execution time by allowing our tasks to run concurrently. This is how coroutines work.
Source Code
import asyncio
import time # for getting execution time
async def eat_food():
print('Start eat_food()')
await asyncio.sleep(5) # I eat for 5 mins.
print('End eat_food()')
return 'Finished eating'
async def watch_video():
print('Start watch_video()')
await asyncio.sleep(8) # I watch an 8 min. video
print('End watch_video()')
return 'Finished watching'
async def main():
start_time = time.time()
task_eat = asyncio.create_task(eat_food())
task_watch = asyncio.create_task(watch_video())
result_eat = await task_eat # 'Finished eating'
result_watch = await task_watch # 'Finished watching'
end_time = time.time()
execution_time = end_time - start_time
print(f'Result of eat_food(): {result_eat}')
print(f'Result of watch_video(): {result_watch}')
print(f'Total execution time: {execution_time:.2f} seconds')
if __name__ == '__main__':
asyncio.run(main())
Conclusion
By now, you have at least the basic idea of the difference between synchronous and asynchronous programming, what kind of function is used in the asynchronous approach, and how to create a basic async program in Python.
In a later post, I'll show you how to apply these concepts on real life by building a basic GUI app.
Key Terms
- Synchronous - tasks are performed one at a time
- Asynchronous - multitasking; also known as async
- Routine - a block of code that can be called as needed; also known as function
- Coroutine - a special kind of function that can be paused and resumed
- asyncio - built-in Python library for doing asynchronous programming
Thanks for reading! I hope you enjoyed this article. If you have some suggestions or criticism, feel free to email me🙂
See you on my next post 😉
Top comments (0)