Python Multi-Threading and the Office

Ankush Kumar Singh
The Startup
Published in
7 min readAug 30, 2020

I have always been a huge fan of concurrent programming and famous sitcom television series The Office. In this blog I am trying to relate multi-threading in Python with different episodes of The office.

THREADS

In sequential programming, like a program (process) to print “Hello World!”, the program has a start, a set of sequence of instructions to execute and an end.

Just like sequential programming, a thread has a start, a set of sequence of instructions to execute and an end. But threads cannot coexist without a process. In other words, threads are a subset of a process. Operating system schedules execution of the threads based on a scheduling algorithm.

As an analogy one can visualize television series “The Office” as a process and every character in the television series as an individual thread. The office show has a beginning, a set of sequence of instructions (or episodes) and an end, just like each of the characters (threads). Characters play their part and are terminated either when the script demands (operating system)or when the show ends (process).

LIFECYCLE OF A THREAD

Broadly speaking thread has 4 states. New, when thread is defined. Runnable is when it has all the resources required to go into execution state.Waiting, when thread is waiting for a resource and Terminated when thread is done with execution.

IMPLEMENTING THREADS

Python provides a threading library for multi-threading implementation. Let us consider the code sample below. Following code depicts “Michael Scott’s Dunder Mifflin Scranton Meredith Palmer Memorial Celebrity Rabies Awareness Pro-Am Fun Run Race for the Cure” fun run, to implement threads.

import threading
import time
def get_lamp():
``` When life gives you a race you buy a lamp from yard sale ```
time.sleep(1)
def get_beer():
``` Well beer always gets preference over running ```
time.sleep(5)
def running():
``` Everyone is running hard. Well almost everyone! ```
name = threading.current_thread().getName()
print(f"{name} started running...")
if name == "Jim" or "Pam":
get_lamp()
if name == "Stanley":
get_beer()
time.sleep(0.5)
print(f"{name} Finished race")
if __name__ == '__main__':
characters = ["Toby", "Michael", "Dwight", "Angela",
"Jim", "Pam", "Stanley"]
threads = list()
# Defining different threads for each character
for character in characters:
threads.append(threading.Thread(target=running,
name=character))
# This will start the threads
for thread in threads:
thread.start()
print(f'active thread count: {threading.active_count()}')
# Waiting for all the threads to finish execution
for thread in threads:
thread.join()
print("Race is finished")

Sample output

Toby started running...
Michael started running...
Dwight started running...
Angela started running...
Jim started running...
Pam started running...
Stanley started running...
active thread count: 8
Toby Finished race
Michael Finished race
Pam Finished race
Angela Finished race
Jim Finished race
Dwight Finished race
Stanley Finished race
Race is finished

In main function we have defined all the characters which will be participating in the race.Function running is executed by multiple threads. In code snippet, thread.start() will start execution of the the thread. However, while running Jim and Pam found a yard sale and decided to get a lamp. This is represented by get_lamp function. While Stanley being Stanley, preferred a beer over a run. Thread Stanley calls get_beer function. To wait for all the threads to finish execution, thread.join is used. When all the threads return to main function after execution, race is finished.

MUTUAL EXCLUSION

Threads does make program faster but causes other issues when multiple threads read and write on shared data. Such a part of code which has a shared resource is called critical section. Python threading module provides Lock to protect the critical section.

import threading
import time
count_lock = threading.Lock()
MAX_THREAD = 10
def thanking_you():
name = threading.current_thread().getName()
count_lock.acquire()
print(f'{name} would like to thank you!')
count_lock.release()
if __name__ == '__main__':
for thread_count in range(MAX_THREAD):
threading.Thread(target=thanking_you, name='Dwight').start()
threading.Thread(target=thanking_you, name='Andy').start()
print("Enough of thanking already!")

Sample output

Dwight would like to thank you!
Andy would like to thank you!
Dwight would like to thank you!
Andy would like to thank you!
Dwight would like to thank you!
Andy would like to thank you!
Dwight would like to thank you!
Andy would like to thank you!
Dwight would like to thank you!
Andy would like to thank you!
Dwight would like to thank you!
Andy would like to thank you!
Dwight would like to thank you!
Andy would like to thank you!
Dwight would like to thank you!
Andy would like to thank you!
Dwight would like to thank you!
Andy would like to thank you!
Dwight would like to thank you!
Andy would like to thank you!
Enough of thanking already!

In the example of above, 2 threads Dwight and Andy, who keep thanking each other till MAX_THREAD count in reached. Print statement to thank is the critical section. Thread Dwight will first execute function thanking_you and acquire thank_lock, which prohibits thread Andy to execute critical section. Thread Andy will wait for thread Dwight to release the lock before executing critical section.

Let’s modify the thanking_you function a little.

def thanking_you():
name = threading.current_thread().getName()
thank_lock.acquire()
for i in range(5):
thank_lock.acquire()
print(f'{name} would like to thank you!')
thank_lock.release()

In the code snippet above, each thread would like to thank 5 times in a row. And for every print statement thread will try to acquire the lock. This leads to a problem.

Let’s say thread Dwight is the first thread executing function thanking_you. It can only acquire lock thank_lock when it’s in released state. In first iteration of the for loop when thread Dwight is executing critical function, it acquires the lock. In second iteration it tries to acquire the lock again which thread Dwight already has. Since lock is not in released state, thread Dwight will wait on itself forever to acquire the lock. To resolve such an issue we can use Rlock from the threading library.

import threading
import time
thank_lock = threading.RLock()
MAX_THREAD = 10
def thanking_you():
name = threading.current_thread().getName()
thank_lock.acquire()
for i in range(5):
thank_lock.acquire()
print(f'{name} would like to thank you!')
thank_lock.release()
if __name__ == '__main__':
threads = list()
threads.append(threading.Thread(target=thanking_you,
name='Dwight'))
threads.append(threading.Thread(target=thanking_you,
name='Andy'))
# starting threads
for thread in threads:
thread.start()
# waiting for threads to finish
for thread in threads:
thread.join()
print("Enough of thanking already!")

Sample output

Dwight would like to thank you!
Dwight would like to thank you!
Dwight would like to thank you!
Dwight would like to thank you!
Dwight would like to thank you!
Andy would like to thank you!
Andy would like to thank you!
Andy would like to thank you!
Andy would like to thank you!
Andy would like to thank you!
Enough of thanking already!

A reentrant lock (Rlock) can be used to acquire the lock by the thread that already has the lock acquired.

PASSING ARGUMENTS TO A FUNCTION USING THREADS

Till now we have been calling a function which doesn’t accept any argument in function call. I am going to use classic, inspirational, thriller, critically acclaimed, bold, highly original and my all time favorite movie, “Threat level midnight!” to demonstrate this.

import threading
import time
characters_survival_dict = dict()def thread_level_midnight(character=None):
# list of characters killed in the movie
killed_characters_list = ["Toby", "Oscar", "Creed"]
if character in killed_characters_list:
characters_survival_dict[character] = "Killed"
else:
characters_survival_dict[character] = "Alive"
if __name__ == '__main__':
characters = ["Toby", "Michael", "Dwight", "Angela",
"Jim", "Pam", "Stanley", "Oscar", "Creed"]
threads = list()
# Defining different threads for each character
for character in characters:
threads.append(threading.Thread(
target=thread_level_midnight,
kwargs=dict(character=character),
name=character)
)
# This will start the threads
for thread in threads:
thread.start()
print(f'active thread count: {threading.active_count()}')
# Waiting for all the threads to finish execution
for thread in threads:
thread.join()
print("Michael Scarn hates:")
for character in characters_survival_dict:
if characters_survival_dict.get(character) == "Killed":
print(character)
print("Threat level what...")
print("midnight!")

Sample output

Michael Scarn hates:
Toby
Oscar
Creed
Threat level what...
midnight!

In the code above, function thread_level_midnight accepts character as an argument. When defining a thread, kwargs can be used to pass the argument to function thread_level_midnight.

Let’s enhance the function thread_level_midnight to return a value.

import threading
import time
import queue
characters_survival_queue = queue.Queue()def thread_level_midnight(character=None):
# list of characters killed in the movie
killed_characters_list = ["Toby", "Oscar", "Creed"]
if character in killed_characters_list:
return character
if __name__ == '__main__':
characters = ["Toby", "Michael", "Dwight", "Angela",
"Jim", "Pam", "Stanley", "Oscar", "Creed"]
threads = list()
# Defining different threads for each character
for character in characters:
threads.append(
threading.Thread(target=lambda q, arg1: \
q.put(thread_level_midnight(arg1)),
args=(characters_survival_queue,
character))
)
# This will start the threads
for thread in threads:
thread.start()
print(f'active thread count: {threading.active_count()}')
# Waiting for all the threads to finish execution
for thread in threads:
thread.join()
print("Michael Scarn hates:")
while not characters_survival_queue.empty():
result = characters_survival_queue.get()
if result: print (result)
print("Threat level what...")
print("midnight!")

Sample output

Michael Scarn hates:
Toby
Oscar
Creed
Threat level what...
midnight!

In the example code above we have used queue to store return values from the function thread_level_midnight.

This was the part one of the blog on python multi-threading. In next part we will see issues caused by locking if not implemented properly.

--

--