Scripting

An Introduction to Asynchronous Programming in Python

An Introduction to Asynchronous Programming in Python

Introduction

When we talk about coding, we usually think about writing code in a synchronous manner. That is obvious, we have heard that the human brain works great when it concentrates upon a single task. I would not say that it is by coincidence that writing synchronous code is second to nature (especially for noobs).

Synchronous code has been the prevalent and still the preferred way of writing code because it has several advantages:

  • Easy to understand
  • Easier to write.
  • No need to consider the resources being shared among multiple processes/threads/calls.
  • No threats for deadlocks (those sleepless nights).

However, synchronous code is slow. They blосk the exeсutiоns оf consecutive орerаtiоns until the under-process task is соmрleted.

Web services are expected to give responses back in a few hundred millis or at most 5 seconds for a good user experience. The huge machine learning models need to process gigabytes of data and these models are not built perfectly in one shot; there are multiple iterations.

So what are our options?

The paradigm of Asynchronous programming comes to the rescue in such cases. Asynchronous programs do not wait for a code to execute, but jumps on the other task and start executing it (well, not always but you get the idea 🙂 ).

Asynchronous рrоgrаmming is а fоrm оf раrаllel соmрuting in whiсh а unit оf tаsk runs seраrаtely frоm the mаin аррliсаtiоn threаd аnd notifies the саlling threаd оf its соmрletiоn, fаilure, оr рrоgress.

Let us take it slow. We are going to divide this topic into two separate articles to get a grasp of the topics.

Let us talk about Python How To’s

Here we are going to discuss Multiprocessing & Multithreading approaches.

Multiprocessing

Usually, applications run a single process for performing any task, thus called uniprocessor systems or applications.

However, the mighty python provides us an option for multiple processes running in parallel. What does it mean?

Think about having two python terminals open and running code on them, execution on one terminal can be done without waiting for the other.

from multiprocessing import Process

import os

import time

import random

def getProcessInfo(title):
  print(title)
  print('module name:', __name__)
  print('parent process:', os.getppid())
  print('process id:', os.getpid())
  print('\n')

def printOne(n: int = 10):
  getProcessInfo('printOne Function Info')
  for _ in range(n):
    time.sleep(random.randint(0, 3))
    print("One")

def printTwo(n: int = 10):
  getProcessInfo('printTwo Function Info')
  for _ in range(n):

    time.sleep(random.randint(0, 3))
    print("Two")

if __name__ == "__main__":
  procs = []

  # Create a Process for printOne function
  proc = Process(target=printOne, args=(10,))
  # Append the Process to the list of all Processes
  procs.append(proc)
  #Start the Process
  proc.start()

  # Create a Process for printTwo function
  proc = Process(target=printTwo)

  # Append the Process to the list of all Processes
  procs.append(proc)

  # Start the Process
  proc.start()

  # Wait until all the processes are executed
  for proc in procs:
    proc.join()

Here we have created two functions that print a string whenever they are run. If we check the output, we can confirm that they are run independently and do not wait for one another.

What is happening here?

Consider we have a dual-core computer each is assigned a core. The methods are run independently in those cores. These multiрrосessоrs shаre the соmрuter bus, CPU сlосk, memоry, аnd the рeriрherаl deviсes.

(If there are more processes than cores, it is a different story and we shall cover that separately.)

Multithreading

Multiple processes sound cool, but sadly they are not very optimal every time we need to do something in parallel.

We know our CPUs have cores, which act as virtual individual CPUs. These virtual CPUs have threads in them. They handle the execution of sequences of instructions in our program.

The name hopefully makes sense right? Running multiple threads aka. multithreading.

from threading import Thread

import os

import time

import random

def getProcessInfo(title):
  print(title)
  print('module name:', __name__)
  print('parent process:', os.getppid())
  print('process id:', os.getpid())
  print('\n')

# will run on Thread 1
def printOne(n: int = 10):
  getProcessInfo('printOne Function Info')
  for _ in range(n):
    time.sleep(random.randint(0, 3))
    print("One")

# will run on Thread 2
def printTwo(n: int = 10):
  getProcessInfo('printTwo Function Info')
  for _ in range(n):
    time.sleep(random.randint(0, 3))
    print("Two")

if __name__ == "__main__":
  threads = []

  # Create a Thread for printOne function
  thread = Thread(target=printOne, args=(10,))

  # Append the Thread to the list of all Threads
  threads.append(thread)
  thread.start()

  # Create a Thread for printTwo function
  thread = Thread(target=printTwo)

  # Append the Thread to the list of all Threads
  threads.append(thread)
  thread.start()

  # Wait until all the threads are executed
  for thread in threads:
    thread.join()

Notice it is the same process for both the threads.

Now, what is happening here?

Here we have a single process which in-turn has multiple threads. Code, data, and files are shared among threads and each thread has a stack.

Conclusion:

So now we can make our applications performant and efficiently use CPU cycles and threads. However, it is not all rainbows and unicorns when talking about asynchronous code.

  • The developer should have a clear idea about what they are doing.
  • It is not trivial to use async code and readability suffers as well.
  • Do not forget about the deadlocks!

Do remember, with great powers come great responsibilities!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *