Education STEM

Multi-threading in Python

Most of the python code you encounter while searching online is single-threaded and in most cases, it’s perfectly fine. Such programs has one main thread in the code and it may call various functions one after another or conditionally…the cpu will execute the code in each line of code one after another until the program ends. The code itself is not doing anything in parallel. As a result, there’s no race conditions or thread locking chance, and the execution time is the sum of all of the code executed sequentially.

You can visualize a single-threaded app execution as below:

single-threaded app

We can implement a single-threaded application in the following example, and let’s time it.

def calc_square(numbers):
    print("calculate square numbers")
    for n in numbers:
        time.sleep(0.2) # in seconds. this artificial delay is just to simulate a busy calculation
        print('square:',n*n)

def calc_cube(numbers):
    print("calculate cube of numbers")
    for n in numbers:
        time.sleep(0.2) # in seconds. this artificial delay is just to simulate a busy calculation
        print('cube:',n*n*n)


def nested_loopadds(maxint):
    n=m=0
    print("nested loop additions")
    for n in range(0, maxint):
        n +=1        
        for m in range(0, maxint):
            m +=1
    print("finished nested_loopads")

Now that we’ve defined these three functions, let’s call them from the main driver code.

listofnums = [2,3,8,9] # a list to calculate sqr and cubes for.

start_time = time.time()
calc_square(listofnums)
calc_cube(listofnums)
nested_loopadds(5000)
print("Took (sec): ", time.time() - start_time)

The output from this small program looks like this on my machine:

out:
    calculate square numbers
    square: 4
    square: 9
    square: 64
    square: 81
    calculate cube of numbers
    cube: 8
    cube: 27
    cube: 512
    cube: 729
    nested loop additions
    finished nested_loopads
    Took (sec):  3.8570058345794678

As you can see the function calc_square() had to completely finish before cpu could execute code in calc_cube() and after that finished, it executed the code in nested_loopadds() before exiting. The whole process took about 3.9 seconds.

With multi-threading, we can spawn three different threads (in addition to the main thread already there) and execute code on each thread independent of the other. And if we attach one of the functions above to each thread, then we can execute all three functions almost simultaneously.

You can visualize a single-threaded app execution as below:

multi-threaded app

This can, in theory, save time. Let’s see how we can do that and also time the new program’s execution time.

import threading
import time
# ...define the worker functions and variables here as we did above

th1= threading.Thread(target=calc_square, args=(listofnums,))
th2= threading.Thread(target=calc_cube, args=(listofnums,))
th3= threading.Thread(target=nested_loopadds, args=(5000,))

th3.start()
th1.start()
th2.start()

th1.join()
th2.join()
th3.join()

print("Took (sec): ", time.time() - start_time) 
print("All threads complete.")

We import threading library for multithreading functions we need.

threading.Thread() creates a new thread object and allows us to connect a function (worker function) to that thread along with parameters needed for that function. The parameters must be in tuple format.

To actually start the threads, we have to call threadobj.start() for each thread.

The threadobj.join() for each threads instructs each thread to join the main thread after all threads and their worker functions are finished, so the main thread resumes till the end of the program.

The time spent with this method on my cpu was: ~2.3 secs and the output looks like this:

out:
    nested loop additions
    calculate square numbers
    calculate cube of numbers
    square: 4
    cube: 8
    square: 9cube:
     27
    square: 64cube:
     512
    square: cube:81
     729
    finished nested_loopads
    Took (sec):  2.319817066192627
    All threads complete.

Notice how we get output from nested_loopadds() in th3 and then calculation moves to calculating square from th1, and cube from th2 in parallel.
All three threads are working in parallel without waiting for one to finish until after all calculations are done and they all return to the main thread.

Even in this small program, we see a significant time saving. The more time-consuming the worker functions, the more pronounced the difference between multithreaded and singlethreaded versions will be.

I hope this was educational. Until next time…



Interested in creating programmable, cool electronic gadgets? Give my newest book on Arduino a try: Hello Arduino!

Leave a Reply

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

Back To Top