top of page

Threads in Python

Updated: Jun 8, 2020

Threads help speed up your application. You can run your code parallelly and threads can help you to simplify your design. If you want to perform an independent task it is better to create a thread. Creating a thread will allow your main thread/process to execute.

Before creating a thread, let’s refresh some key features of thread.


What is a thread?

  • Thread is called “light weight process”.

  • A thread is a separate flow of execution.

  • In a process, there may be one or more threads running simultaneously.

  • Each thread performs a specific task.

  • It takes less time for creation and context switching than process.

  • Threads share memory address space. Threads of a process share its virtual address space and system resources.

  • Each thread maintains exception handlers, a scheduling priority, thread-local storage, a unique thread identifier, and a set of structures the system will use to save the thread context until it is scheduled.

What is the need of thread?

  • Multithreading can help speed up tasks that are IO-bound.

  • I/O may be from a network, database, file etc.

  • These I/O usually takes a significant amount of time to perform.

  • Threading can add more clarity in design.

A Python Thread

Python offers 2 modules for thread creation.

  • _thread – It provides a low-level threading API.

  • threading(threading.py) - This module constructs higher-level threading interfaces on top of the lower level _thread module.

This blog will explain thread creation using the “threading” module as it gives lots of helpful APIs which you will not get in low level “_thread” module.

There are 3 ways we can create a thread in python.

  • Without any class

  • By extending Threading class

  • Using class without extending a thread class.

Creating a thread

class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

threading module provides a Thread class. Thread class constructor takes keyword arguments and you should always call the constructor with keyword parameters.

group – None. Reserved for future use

target – pass a callable function/class function/callable object to be invoked by run() method.

Name – name of the thread. If no name is passed, the default name of the thread is “Thread-n” where n is a decimal number.

Args – arguments to the thread. This parameter is a tuple.

kwargs - is a dictionary of keyword arguments for the target invocation.

Daemon - sets whether the thread is daemonic. If None, the daemonic property is inherited from the current thread.

Daemon threads are stopped at shutdown. Resources acquired by this thread may not be released properly.


Method 1:

import threading
count1 = 0
def even(thread_name):
    global count1
    while count1 < 10:
        if count1 % 2 == 0:
            print (thread_name + ": \t", count1)
        count1 = count1 + 1
 
thread1 = threading.Thread(target=even, args=("Even",), name="test1")
thread1.start()
thread1.join()
print("Done.")

Output:

you will have to import the “threading” module to create a thread. "even()” function will print all the even numbers between 0 to 10.

After creating a thread object, you need to call start() function. This invokes run() method of Thread class.

Once thread started to run, you need to attach this thread to the main thread by calling join() method of Thread class. It joins thread1 thread to main thread and main thread waits for thread1 to complete. If you forget to join two threads, the main thread will continue to execute and exit on completion. In this case, thread1 will be left as an orphan.


Method 2:

import threading

count = 0
class MyThread():
    def even(self, thread_name):
        global count
        while count < 10:
            if count % 2 == 0:
                print(thread_name + ": \t", count)
            count = count + 1

obj = MyThread()
thread = threading.Thread(target=obj.even, args=("Even",), name="EvenThread")
thread.start()
thread.join()
print("Done.")

Output:

This is another way of creating a thread using a class member method. A thread will run the target function.


Method 3:

import threading

count = 0
class MyThread(threading.Thread):
    def __init__(self,thread_name):
        super(MyThread, self).__init__(args=(thread_name,), name=thread_name)
        self.thread_name = thread_name

    def run(self):
        global count
        while count < 10:
            if count % 2 == 0:
                print(self.thread_name + ": \t", count)
            count = count + 1

thread = MyThread("Even")
thread.start()
thread.join()
print("Done.")

Output:

You can extend threading.Thread class in your sub-class. When you are extending Thread class, remember to implement a run() function which will be called internally when you will call start() method.

Thread local data

Python threading module offers a useful class – threading.local.

import threading

count = 0
def foo(thread_name, num):
    global count
    local_data = threading.local()
    local_data.local_name = thread_name
    local_data.x = 2 * num
    while count < 50:
        if count % 2 == 0:
            print(local_data.x,"- \t" + local_data.local_name + ": \t", count)
        count = count + 1

thread1 = threading.Thread(target=foo, args=("Thread1",2))
thread2 = threading.Thread(target=foo, args=("Thread2",3))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print("Done.")

Output:

Thread local data is the data whose values are specific to threads. To have thread-local data, create an instance of threading.local class. Instance if this class can have any number of thread-local variables.

In the above example, there are two thread-local variables - local_data.local_name and local_data.x

Note: thread1 and thread2 are not synchronized. So output may vary in each run.

More about Thread class

  • Name - A string used for identification purposes only.

  • getName() / setName() - getter/setter API for name

  • ident - The ‘thread identifier’ of this thread or None

  • native_id - The native integral thread ID of this thread.

  • is_alive() - Return whether the thread is alive.

  • Daemon - A boolean value indicating whether this thread is a daemon thread (True) or not (False).


Some features of the threading module

  • threading.active_count() - Return the number of Thread objects currently alive.

  • threading.current_thread() - Return the current Thread object, corresponding to the caller’s thread of control.

  • threading.get_ident() - Return the ‘thread identifier’ of the current thread.

  • threading.get_native_id() - Return the native integral Thread ID of the current thread assigned by the kernel.

  • threading.enumerate() - Return a list of all Thread objects currently alive.


Important Info

Having multiple threads in a python application may not speed up. Due to Global Interpreter Lock (GIL), only one thread can execute. It means, irrespective of having a multi-core machine, only one thread will execute at a time.

If you want to better use your multicores, use the multiprocessing module. I will discuss this module in another blog.

Global Interpreter Lock: From python documentation

The mechanism used by the CPython interpreter to assure that only one thread executes Python bytecode at a time. This simplifies the CPython implementation by making the object model (including critical built-in types such as dict) implicitly safe against concurrent access. Locking the entire interpreter makes it easier for the interpreter to be multi-threaded, at the expense of much of the parallelism afforded by multi-processor machines.

Conclusion

I hope now you have a pretty good understanding of python thread and the ways to create threads. Thread creation is a quite simpler task than thread synchronization. Most of the newcomers struggle in thread synchronization. It seems tough in the beginning but once you understand the core concept of synchronization, it will become easy. I will discuss thread synchronization methods in my next blog.


Comments


© 2023 by Dheeraj Jha

bottom of page