Python multithreading: Introduce concurrency into your programs.
By: Mikel Zabalza
Tags: Concurrency, Python, Multithreading

When tasks are identical, only differ from the data they operate on, and can be executed in any order, the problem is said to be embarrassingly parallel

Concurrency works best in algorithms that require the execution of a specific action n number of times. If this action is outside our computer scope (waiting for the response of an api) multithreading is the perfect option in order to minimize unused computing resources.

Python is often pointed as difficult to use for multithreaded tasks. However the ThreadPoolExecutor module part of the concurrent.futures package (part of the Python standard library)provides us with an easy way to do it without major changes in our original code.

Use case: Multiple api calls

Lets start with a simple piece of code that will make n requests to the swapi.dev api. SWAPI is a Star Wars API that contains a dataset that can be consumed via REST API.

The first endpoint will give us the number of different people in their dataset:

import requests url = 'https://swapi.dev/api/people/'

r = requests.get(url)

print(r.json()['count'])

# >> 82

and using a person indentifier (identifiers go from 1 to 83) we can get info of one person:

import requests url = 'https://swapi.dev/api/people/1'

r = requests.get(url)

print(r.json()) >>

{

'name': 'Luke Skywalker',

'height': '172',

'mass': '77',

'hair_color': 'blond',

'skin_color': 'fair',

'eye_color': 'blue',

'birth_year': '19BBY',

...

}

Now lets create a list of people ids, to be able to iterate through them making request to the swapi api with each id as input parameter.

Single Threaded

import requests

from datetime import datetime input_ids = list(range(1, 84)) * 5

# Increase the items of the list to simulate higher load

response_list = ['-'] * len(input_ids) def request_person(person_id, idx):

url = f'

try:

r = requests.get(url)

response_list[idx] = r.json()['name']

except Exception as e:

return (person_id, idx):url = f' https://swapi.dev/api/people/{person_id}' r = requests.get(url)response_list[idx] = r.json()['name']e:return start = datetime.now()

for idx, _id in enumerate(input_ids):

request_person(_id, idx) print(datetime.now() - start) >> 0:01:04.925463

Now lets transform it into a multihreaded task with the ThreadPoolExecutor:

Multi Threaded

import requests

from concurrent.futures import ThreadPoolExecutor

from datetime import datetime input_ids = list(range(1, 84)) * 5

response_list = ['-'] * len(input_ids) def request_person(person_id, idx):

url = f'

try:

r = requests.get(url)

response_list[idx] = r.json()['name']

except Exception as e:

return (person_id, idx):url = f' https://swapi.dev/api/people/{person_id}' r = requests.get(url)response_list[idx] = r.json()['name']e:return start = datetime.now()

with ThreadPoolExecutor(max_workers=20) as executor:

[executor.submit(request_person, person_id=_id, idx=idx) for idx, _id in enumerate(input_ids)] print(f'Time taken: {datetime.now() - start}') >> 0:00:10.516984

So basically, adding just few additional lines of code, we have been able to reduce program execution time by 7.

Conclusion

There are many ways to introduce concurrency into our Python programs, the best method to choose is going to depend on your specific use case. Hopefully the example on this article has been helpful to introduce you into the topic and help you to replicate in further cases.