Python Threading examples
ThreadPoolExecutor
The code creates a ThreadPoolExecutor
as a Context Manager. It then uses .map()
which takes a function and an iterable of things. .map()
steps through an iterable of things, in this case network_device_hostnames: list[str]
, passing each one to a thread in the pool.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import concurrent.futures
def example_of_threading(network_device_hostnames: list[str]) -> list:
"""
:param list[str] network_device_hostnames: list of hostnames of network devices
:return: a Generator where each element is information about the network device
"""
def get_network_device_info(hostname: str) -> str:
"""
some logic here
"""
return ""
with concurrent.futures.ThreadPoolExecutor() as executor:
results = executor.map(get_network_device_info, network_device_hostnames)
return results
The most interesting code here is the Context Manager using concurrent.futures.ThreadPoolExecutor
.
1
2
with concurrent.futures.ThreadPoolExecutor() as executor:
results = executor.map(get_network_device_info, network_device_hostnames)
NOTE: The generator result
can easily be converted to a Python list with list(results)
Proof of Concept
First, using ipython
, I will copy the code with a few additions to see the times.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Python 3.9.6 (default, Nov 10 2023, 13:38:27)
Type 'copyright', 'credits' or 'license' for more information
IPython 8.18.1 -- An enhanced Interactive Python. Type '?' for help.
In [1]: import concurrent.futures
...: import datetime
...: from time import sleep
...:
...:
...: def example_of_threading_returning_a_list(network_device_hostnames: list[str]) -> list:
...: """
...: :param list[str] network_device_hostnames: list of hostnames of network devices
...:
...: :return: a Python List where each element is information about the network device
...: """
...: def get_network_device_info(hostname: str):
...: """
...: some logic here
...: """
...: current_time_start = datetime.datetime.now()
...: print(f"start_{hostname}_{current_time_start}")
...:
...: sleep(10)
...:
...: current_time_end = datetime.datetime.now()
...: print(f"end_{hostname}_{current_time_end}")
...:
...: return f"{hostname}_result"
...:
...: with concurrent.futures.ThreadPoolExecutor() as executor:
...: results = executor.map(get_network_device_info, network_device_hostnames)
...:
...: return results
I will simulate a few network device hostnames in a Python list
1
2
3
4
5
6
7
8
9
10
11
12
13
14
In [2]: network_device_hostnames = [
...: "device0",
...: "device1",
...: "device2",
...: "device3",
...: "device4",
...: "device5",
...: "device6",
...: "device7",
...: "device8",
...: "device9",
...: ]
In [3]:
I will finally call the function example_of_threading_returning_a_list
and store its return value in a variable called results
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
In [3]: results = example_of_threading_returning_a_list(network_device_hostnames)
start_device0_2024-07-21 20:31:46.958015
start_device1_2024-07-21 20:31:46.959030
start_device2_2024-07-21 20:31:46.960256
start_device3_2024-07-21 20:31:46.960464
start_device4_2024-07-21 20:31:46.960626
start_device5_2024-07-21 20:31:46.960786
start_device6_2024-07-21 20:31:46.960956
start_device7_2024-07-21 20:31:46.961154
start_device8_2024-07-21 20:31:46.961328
start_device9_2024-07-21 20:31:46.961486
end_device1_2024-07-21 20:31:56.961024
end_device2_2024-07-21 20:31:56.961074
end_device3_2024-07-21 20:31:56.961132
end_device0_2024-07-21 20:31:56.961107
end_device4_2024-07-21 20:31:56.965011
end_device6_2024-07-21 20:31:56.965056
end_device8_2024-07-21 20:31:56.965097
end_device5_2024-07-21 20:31:56.965178
end_device7_2024-07-21 20:31:56.965157
end_device9_2024-07-21 20:31:56.965128
In [4]:
We can see that the function called to each of the 10 network devices, we waited 10 seconds (simulation I/O bound) and we finally got our result back. The result is actually of type generator
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In [4]: type(results)
Out[4]: generator
...
In [6]: next(results)
Out[6]: 'device0_result'
In [7]: next(results)
Out[7]: 'device1_result'
In [8]: next(results)
Out[8]: 'device2_result'
In [9]:
It can be easily converted to a Python List
1
2
3
4
5
6
7
8
9
10
11
In [9]: list(results)
Out[9]:
['device3_result',
'device4_result',
'device5_result',
'device6_result',
'device7_result',
'device8_result',
'device9_result']
In [10]:
Using zip() to create a Dictionary
We may want to have a Dictionary as a return value. We may want this so we can keep track of each inputs passed as a List to the function and the corresponding return values after calling ThreadPoolExecutor
. One way to accomplish this is using zip()
. See the previous example modified.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import concurrent.futures
def example_of_threading(network_device_hostnames: list[str]) -> list:
"""
:param list[str] network_device_hostnames: list of hostnames of network devices
:return: a Generator where each element is information about the network device
"""
def get_network_device_info(hostname: str) -> str:
"""
some logic here
"""
return ""
result_dict = dict()
with concurrent.futures.ThreadPoolExecutor() as executor:
for hostname, result in zip(network_device_hostnames, executor.map(get_network_device_info, network_device_hostnames)):
result_dict[hostname] = result
return result_dict
The most important code snippet now becomes:
1
2
3
4
5
result_dict = dict()
with concurrent.futures.ThreadPoolExecutor() as executor:
for hostname, result in zip(network_device_hostnames, executor.map(get_network_device_info, network_device_hostnames)):
result_dict[hostname] = result
Proof of Concept
We again make use of ipython
to test this code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Python 3.9.6 (default, Nov 10 2023, 13:38:27)
Type 'copyright', 'credits' or 'license' for more information
IPython 8.18.1 -- An enhanced Interactive Python. Type '?' for help.
...: """
...: def get_network_device_info(hostname: str):
...: """
...: some logic here
...: """
...: current_time_start = datetime.datetime.now()
...: print(f"start_{hostname}_{current_time_start}")
...:
...: sleep(10)
...:
...: current_time_end = datetime.datetime.now()
...: print(f"end_{hostname}_{current_time_end}")
...:
...: return f"{hostname}_result"
...:
...: result_dict = dict()
...:
...: with concurrent.futures.ThreadPoolExecutor() as executor:
...: for hostname, result in zip(network_device_hostnames, executor.map(get_network_device_info, ne
...: twork_device_hostnames)):
...: result_dict[hostname] = result
...:
...: return result_dict
...:
...:
...:
...: network_device_hostnames = [
...: "device0",
...: "device1",
...: "device2",
...: "device3",
...: "device4",
...: "device5",
...: "device6",
...: "device7",
...: "device8",
...: "device9",
...: ]
In [2]:
We call example_of_threading_returning_a_list
and store the results in result_dict
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
In [2]: result_dict = example_of_threading_returning_a_list(network_device_hostnames)
start_device0_2024-07-21 20:43:57.597709
start_device1_2024-07-21 20:43:57.598008
start_device2_2024-07-21 20:43:57.598161
start_device3_2024-07-21 20:43:57.598345
start_device4_2024-07-21 20:43:57.598520
start_device5_2024-07-21 20:43:57.598684
start_device6_2024-07-21 20:43:57.598884
start_device7_2024-07-21 20:43:57.599032
start_device8_2024-07-21 20:43:57.599181
start_device9_2024-07-21 20:43:57.599325
end_device0_2024-07-21 20:44:07.602269
end_device1_2024-07-21 20:44:07.602401
end_device6_2024-07-21 20:44:07.602535
end_device9_2024-07-21 20:44:07.602585
end_device2_2024-07-21 20:44:07.602754
end_device4_2024-07-21 20:44:07.602805
end_device7_2024-07-21 20:44:07.602650
end_device8_2024-07-21 20:44:07.602695
end_device3_2024-07-21 20:44:07.603248
end_device5_2024-07-21 20:44:07.602478
In [3]:
We can confirm the return value is a dict
and we managed to store the inputs as keys and the results as values in this dictionary.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
In [3]: type(result_dict)
Out[3]: dict
In [4]: result_dict
Out[4]:
{'device0': 'device0_result',
'device1': 'device1_result',
'device2': 'device2_result',
'device3': 'device3_result',
'device4': 'device4_result',
'device5': 'device5_result',
'device6': 'device6_result',
'device7': 'device7_result',
'device8': 'device8_result',
'device9': 'device9_result'}
In [5]: