Getting Started with Asynchronous Programming in Python
Tags: python, asyncio, asynchronousprogramming
Published: 2024-02-17 14:40:25

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.
![a diagram showing how subroutines work](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/itn6bvmepebjcmemke6v.jpg)

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.
![a diagram showing how coroutines work](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xae90hxwey0yvbe4brj3.jpg)

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.
```python
import time  # for getting execution time
```

Then, we define our functions.
```Python
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.

```Python
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.
```Shell
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
```Python
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.
```Python
import asyncio
import time  # for getting execution time
```

To turn a function into a coroutine, add the `async` keyword to the function.
```Python
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.
```Python
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.
```python
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()`
```Python
def main():
	task_eat = async.create_task(eat_food())
	task_watch = async.create_task(watch_video())
```

**By batch** using `asyncio.gather()`
```Python
def main():
	batch = asyncio.gather(eat_food(), watch_video())
```

To get the return values of coroutines, you need to `await` them

**Individually**
```Python
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**
```Python
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,
```python
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**
```Python
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**
```Python
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.
```python
if __name__ == '__main__':
	asyncio.run(main())
	
```

```Shell
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**
```Python
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](mailto:uzzielynciong@gmail.com) me🙂

See you on my next post 😉