Run multiple threads on Python

Code explanation on how to run a synchronous multi-thread process using python.

In my previous article, I wrote on how to run and properly manage a single thread using the threading library on python. In this post, I am going to step up complexity and show how to run multiple threads on a single code.

If you wish to know more about how threading works, you can refer to this article with an in-depth explanation on what are single threads, multi-threads, synchronous and asynchronous processing.

Note that no matter if you choose to run your code in a synchronous or asynchronous way (this will require the asyncio library), threads will start at the same time (unless specifically coded to be executed one after the other).

When are threads used?

It is very unlikely that you will need to use threads, especially if asynchronous, on tasks that will only require your personal computer, mostly because once you run an algorithm on local, you do not have to worry about resource allocation. Firstly, because the computational power you need is relatively insignificant, and secondly because your computer will not spare extra resources for any computationally intensive process,

However, imagine you are running an online application that is supposed to receive requests from millions of users. If you were just limiting yourself to run a dedicated algorithm for every user without exploiting threads, you could even end up dedicated a single Virtual Machine (an emulated computer that runs your code) for every single user. That would be unmanageable and its price exorbitant. Instead, by using threads you can allocate several users for each virtual machine because now they can run algorithms at the same time, solving you many allocation issues.

However, even if you are not going to use them for a scalable project, it is always useful for any python programmer to know how to work with threads.

Creating threads in Python

Before you start coding, you will need to install the threading library with pip:

!pip install threading

In the following code you will see how I call two threads called thread_1 and thread_2:

import threading

def thread_1_foo(val):
    #time.sleep(sleep_time)
    for a in range(val):
        print('_', 1, a)
    print('thread_1 finished')
  
def thread_2_foo(val):
    #time.sleep(sleep_time)
    for a in range(val):
        print('_', 2, a)
    print('thread_2 finished')

#creating threads
thread_1 = threading.Thread(target=thread_1_foo, args=(100,))
thread_2 = threading.Thread(target=thread_2_foo, args=(100,))

thread_1.start()
thread_2.start()

print("Done!")

Because both threads will start at the same time as the rest of the code, the console will show a mix of all three running simultaneously:

_1 0
Done!
_2 0
_1 1
_2 1
_2 2
_1 2
...
_1 100
_2 100

In reality, the output is much uglier, as lines could have a mix of different prints, I just made the output more comprehensible. This is sufficient to show you how two threads that are called in two consecutive lines of code will still run at the same time.

Using join to make the regular code wait for threads to finish

To avoid this cluster**** we can use a threading method called join(). What this does is to prevent any further code from running until the thread upon which the join is called has not finished. For example, in the following code, we make the last print function (that prints Done!) execute its code after both threads are finished.

import threading

def thread_1_foo(val):
    #time.sleep(sleep_time)
    for a in range(val):
        print('_', 1, a)
    print('thread_1 finished')
  
def thread_2_foo(val):
    #time.sleep(sleep_time)
    for a in range(val):
        print('_', 2, a)
    print('thread_2 finished')

#creating threads
thread_1 = threading.Thread(target=thread_1_foo, args=(100,))
thread_2 = threading.Thread(target=thread_2_foo, args=(100,))

#join waits for the thread to die
#any code after join has to wait for the join process to end
thread_1.join()
thread_2.join()

print("Done!")

The output is the following:

...
_1 100
_2 100
Done!

Using join to run a thread after another one

There is also the possibility of avoiding running both threads simultaneously. This is not useful when you only have two threads (then regular code would be better), but when you have to manage a hundred, the situation changes:

#multi-threading

def thread_1_foo(val):
    #time.sleep(sleep_time)
    for a in range(val):
        print('_', 1, a)
    print('thread_1 finished')
  
def thread_2_foo(val):
    #time.sleep(sleep_time)
    for a in range(val):
        print('_', 2, a)
    print('thread_2 finished')

#creating thread
thread_1 = threading.Thread(target=thread_1_foo, args=(200,))
thread_2 = threading.Thread(target=thread_2_foo, args=(100,))

#join waits for the thread to die
#any code after join has to wait for the join process to end
thread_1.start()
thread_1.join()

thread_2.start()
thread_2.join()

print("Done!")

The output of the code is the following:

_1 0
_1 1
...
_1 100
_2 0
_2 1
...
_2 100
Done!

As you can see, the algorithms run the processes in this order, and only allows each one to start when the previous one has finished:

  • Thread 1
  • Thread 2
  • Regular code

Join our free programming community on discord, learn how to code, and meet other experts

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: