You are currently viewing Python Threading Explained With Examples

Python threading is a powerful feature that allows you to perform multiple tasks simultaneously. Threads are lightweight, independent units of execution that run concurrently within a process. In this article, we will explore how to create, manage, and work with threads in Python.

Advertisements

1. What is Threading in Python?

Threading is a way of achieving multitasking in Python. It allows a program to have multiple threads of execution simultaneously. Each thread runs independently and can perform different tasks concurrently. This means that if one thread is blocked or waiting for input/output, other threads can continue to run and keep the program responsive.

Python provides a threading module that makes it easy to create and manage threads in a program. With this module, you can create multiple threads, start them, and synchronize their execution.

Example of a thread in Python:


# Import threading module
import threading

# Create function
def my_function():
    print("I am text from MyFunc")

# Create a new thread
my_thread = threading.Thread(target=my_function)

# Start the thread
my_thread.start()

When you run this code, you should see the message “I am text from MyFunc” printed to the console.

2. Thread vs Process

In Python, both threads and processes are used for concurrent programming, but they differ in various ways. A process can be thought of as an instance of a program in execution, while a thread is a separate flow of execution within a process.

See the Key differences between thread and process in Python from the following table:

ProcessThread
A process is a program in execution.A thread is a lightweight process.
Processes run independently of each other.Threads share the same memory and can access the same variables and data structures.
Each process has its own memory space.Threads run within the memory space of the process.
Processes are heavyweight and take more resources.Threads are lightweight and require fewer resources.
Processes communicate with each other through interprocess communication (IPC).Threads communicate with each other through shared memory or message passing.
Processes can run on different processors or cores.Threads are limited to a single processor or core.
Thread Vs Process in Python

See the below example where we create one process and one thread:


# Imports
import multiprocessing
import threading

# Define a function to run as a process
def process_function():
    print("This is a process")

# Define a function to run as a thread
def thread_function():
    print("This is a thread")

if __name__ == "__main__":
    # Create a process
    process = multiprocessing.Process(target=process_function)
    process.start()

    # Create a thread
    thread = threading.Thread(target=thread_function)
    thread.start()

3. Why use Python Threading?

In traditional programming, a program runs sequentially from start to finish. Each line of code is executed one after another, and if a particular line of code takes a long time to execute, the program will become unresponsive.

Python threading provides a solution to this problem by allowing multiple threads of execution to run concurrently within a single program. Each thread runs independently of the others so that if one thread becomes blocked waiting for an input/output operation to complete, the other threads can continue executing.

This allows the program to remain responsive even when some parts of it are waiting for I/O operations to complete. However, working with thread is quite painful if you don’t know how threads works in Python.

4. Creating and Starting Threads in Python

The process of creating and starting threads involves creating a thread object, specifying the target function, and then starting the thread using the start() method.

4.1 Creating Threads with threading Module

The threading module in Python provides a simple way to create threads. You can create a new thread by defining a function and then creating a Thread object from that function.


# Import threading
import threading

def print_numbers():
    for i in range(1, 6):
        print(f"Thread 1: {i}")

def print_letters():
    for letter in ["a", "b", "c"]:
        print(f"Thread 2: {letter}")

thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_letters)

thread1.start()
thread2.start()

thread1.join()
thread2.join()

4.2 Creating Threads with the @thread Decorator

Python provides a @thread decorator that allows you to create a thread from a function directly.


# Import 
import threading

def thread(fn):
    def wrapper(*args, **kwargs):
        t = threading.Thread(target=fn, args=args, kwargs=kwargs)
        t.start()
        return t
    return wrapper

@thread
def my_function():
    print("Hello from a thread")

my_function()

4.3 Joining Threads to Wait for Completion

You can use the join() method to wait for a thread to complete its execution before moving on to the next task. The join() method blocks the calling thread and waits for the specified thread to terminate.


import threading

def worker():
    print("Worker thread executing")

# Create a new thread
t = threading.Thread(target=worker)

# Start the thread
t.start()

# Wait for the thread to finish
t.join()

print("Thread Finished")

5. Thread Instance Attributes

In Python threading, threads are instances of the Thread class. This class has several attributes that allow you to query or modify various aspects of the thread instance.

5.1 Query Thread Name

Thread Name is a readable identifier for the thread. By default, Python assigns a name to each thread created, such as “Thread-1”, “Thread-2”, and so on. However, you can also assign a custom name to the thread.


import threading

def my_function():
    print("Thread name:", threading.current_thread().getName())

t = threading.Thread(target=my_function, name="MyThread")
t.start()

# Alternatively, you can use the setName() method
t.setName("NewThreadName")
print("Thread name:", t.getName())

Yields the following output:


Thread name: MyThread
Thread name: NewThreadName

5.2 Query Thread Identifier

You can obtain the identifier of a thread by calling the ident attribute on the thread instance. This identifier is a non-negative integer that uniquely identifies the thread within the program.


import threading

def my_function():
    print(f"Identifier: {threading.get_ident()}")

my_thread = threading.Thread(target=my_function)
my_thread.start()

6. What are Daemon Threads?

A daemon thread is a thread that runs in the background and is killed automatically when the main program finishes. The daemon threads are useful for tasks that don’t need to be completed before the program ends, such as background tasks that can be stopped at any time without causing any harm.

Daemon threads are created using the setDaemon() method of the Thread class. When a thread is created, it inherits the daemon status of its parent thread by default.


import threading
import time

def worker():
    print("Worker started")
    time.sleep(2)
    print("Worker finished")

t = threading.Thread(target=worker)
# Set the thread as a daemon
t.setDaemon(True)   
t.start()

print("Main thread finished")

# Output:
# Worker started      
# Main thread finished

The daemon attribute can also be used to configure whether the thread is a daemon thread or not.


my_thread.daemon = True
print(my_thread.daemon)  

# Output: True

7. Using a ThreadPoolExecutor

The concurrent.futures module provides a ThreadPoolExecutor class . This class allows you to execute a large number of threads in a thread pool. The ThreadPoolExecutor can be used to simplify the management of multiple threads, by allowing you to submit multiple tasks to the executor and have them run in parallel on the thread pool.

To use the ThreadPoolExecutor, you first create an instance of the class by specifying the maximum number of threads to use in the pool.


import concurrent.futures

# define a function to be run in parallel
def task(number):
    print(f"Starting task {number}")
    result = number * 2
    print(f"Task {number} result: {result}")
    return result

# create a thread pool with 4 threads
executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)

# submit 10 tasks to the executor
futures = [executor.submit(task, i) for i in range(10)]

# wait for all tasks to complete
concurrent.futures.wait(futures)

# print the results of all tasks
for future in futures:
    print(future.result())

You can submit tasks to the executor using the submit() method, which returns a Future object that represents the execution of the task. You can use the result() method of the Future object to get the result of the task when it’s done.

8. Summary and Conclusion

We have covered the basics of threading in Python, including what threads are, how they differ from processes, and why they are useful. There are different ways to create and start threads, as well as how to query and configure their attributes. As always, if you have any questions or feedback, feel free to leave a comment below.

Happy coding!