Easy to Use Threading

PythonPythonIntermediate
Practice Now

Introduction

In this tutorial, we will learn how to use Python's threading module to run multiple threads of execution concurrently.

Python's threading module provides a simple way to create and manage threads in a Python program. A thread is a separate flow of execution within a program. By running multiple threads concurrently, we can take advantage of multi-core CPUs and improve the performance of our programs.

The threading module provides two classes for creating and managing threads:

  1. Thread class: This class represents a single thread of execution.
  2. Lock class: This class allows synchronizing access to shared resources between threads.

Creating Threads

To create a new thread in Python, we need to create a new instance of the Thread class and pass it a function to execute.

Create a Project called create_thread.py in the WebIDE and enter the following content.

import threading

## Define a function to run in the thread
def my_function():
    print("Hello from thread")

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

## Start the thread
thread.start()

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

## Print "Done" to indicate that the program has finished
print("Done")

This example defines a function my_function that prints a message. We then create a new instance of the Thread class, passing it my_function as the target function. Finally, we start the thread using the start method and wait for it to finish using the join method.

Use the following command to run the script.

python create_thread.py

Synchronization

If multiple threads access the same shared resource (e.g., a variable or file), we must synchronize access to that resource to avoid race conditions. Python's threading module provides a Lock class for this purpose.

Here's an example of using a Lock to create a Project called sync.py in the WebIDE and enter the following content.

import threading

## Create a lock to protect access to shared resource
lock = threading.Lock()

## Shared resource that will be modified by multiple threads
counter = 0

## Define a function that each thread will run
def my_function():
    global counter

    ## Acquire the lock before accessing the shared resource
    lock.acquire()
    try:
        ## Access the shared resource
        counter += 1
    finally:
        ## Release the lock after modifying the shared resource
        lock.release()

## Create multiple threads to access the shared resource
threads = []

## Create and start 10 threads that execute the same function
for i in range(10):
    thread = threading.Thread(target=my_function)
    threads.append(thread)
    thread.start()

## Wait for all threads to complete their execution
for thread in threads:
    thread.join()

## Print the final value of the counter
print(counter) ## Output: 10

We create a Lock object and a shared resource counter in this example. The my_function function accesses the shared resource by acquiring the lock using the acquire method and releasing the lock using the release method. We create multiple threads and start them, then wait for them to finish using the join method. Finally, we print the final value of the counter.

Use the following command to run the script.

python sync.py

Thread with Arguments

In Python, you can pass arguments to threads by either using the args parameter when creating a new thread or by subclassing the Thread class and defining your constructor that accepts arguments. Here are examples of both approaches:

1; We create a subclass of the Thread class and override the run method to define the thread's behavior. Create a Project called thread_subclass.py in the WebIDE and enter the following content.

import threading

## Define a custom Thread class that extends the Thread class
class MyThread(threading.Thread):
    ## Override the run() method to implement thread's behavior
    def run(self):
        print("Hello from thread")

## Create an instance of the custom thread class
thread = MyThread()

## Start the thread
thread.start()

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

Use the following command to run the script.

python thread_subclass.py

2; We create a thread and pass arguments to the target function using the args parameter. Create a Project called thread_with_args.py in the WebIDE and enter the following content

import threading

## Define a function that takes a parameter
def my_function(name):
    print("Hello from", name)

## Create a new thread with target function and arguments
thread = threading.Thread(target=my_function, args=("Thread 1",))

## Start the thread
thread.start()

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

Use the following command to run the script.

python thread_with_args.py

Thread Pool

In Python, you can use a thread pool to execute tasks concurrently using a predefined set of threads. The benefit of using a thread pool is that it avoids the overhead of creating and destroying threads for each task, which can improve performance.

Python's concurrent.futures module provides a ThreadPoolExecutor class that allows you to create a pool of threads and submit tasks. Here's an example:

Create a Project called thread_pool_range.py in the WebIDE and enter the following content.

import concurrent.futures

## Define a function to be executed in multiple threads with two arguments
def my_func(arg1, arg2):
    ## Define the tasks performed by the thread here
    print(f"Hello from my thread with args {arg1} and {arg2}")

## Create a ThreadPoolExecutor object with a maximum of 5 worker threads
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    ## Submit each task (function call with its arguments) to the executor for processing in a separate thread
    ## The submit() method returns a Future object representing the result of the asynchronous computation
    for i in range(10):
        executor.submit(my_func, i, i+1)

In this example, we define a function my_func that takes two arguments. We create a ThreadPoolExecutor with a maximum of 5 worker threads. Then, we loop through a range of numbers and submit tasks to the thread pool using the executor.submit() method. Each submitted task is executed on one of the available worker threads.

Tips: The ThreadPoolExecutor object is used as a context manager. This ensures that all threads are properly cleaned up when the code inside the with block is completed.

Use the following command to run the script.

python thread_pool_range.py

The submit() method returns a Future object immediately, representing the submitted task's result. You can use the result() method of the Future object to retrieve the task's return value. If the task raises an exception, calling result() will raise that exception.

You can also use the map() method of the ThreadPoolExecutor class to apply the same function to a collection of items. For example, Create a Project called thread_pool_map.py in the WebIDE and enter the following content.:

import concurrent.futures

## Define a function to be executed in multiple threads
def my_func(item):
    ## Define the tasks performed by the thread here
    print(f"Hello from my thread with arg {item}")

## Create a list of items to be processed by the threads
items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

## Create a ThreadPoolExecutor object with a maximum of 5 worker threads
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    ## Submit each item to the executor for processing in a separate thread
    ## The map() method automatically returns the results in order
    executor.map(my_func, items)

In this example, we define a function my_func that takes one argument. We create a list of items and submit them to the thread pool using the executor.map() method. Each item in the list is passed to my_func as an argument, and each item is executed on one of the available worker threads.

Use the following command to run the script.

python thread_pool_map2.py

The results obtained from thread_pool_range.py and thread_pool_map.py are the same.

Daemon Threads

In Python, a daemon thread is a type of thread that runs in the background and does not prevent the program from exiting. When all non-daemon threads have been completed, the Python interpreter exits, regardless of whether any daemon threads are still running.

Create a Project called daemon_thread_with_args.py in the WebIDE and enter the following content.

import threading
import time

## Define a function that runs indefinitely and prints messages at a regular interval
def my_function():
    while True:
        print("Hello from thread")
        time.sleep(1)

## Create a daemon thread that runs the target function
thread = threading.Thread(target=my_function, daemon=True)

## Start the thread
thread.start()

## The main program continues to execute and prints a message
print("Main program")

## Wait for a few seconds before exiting the program
time.sleep(5)

## The program exits, and the daemon thread is terminated automatically
print("Main thread exiting...")

In this example, we create a thread that runs an infinite loop and prints a message every second using time.sleep() function. We mark the thread as a daemon using the daemon parameter to exit when the main program exits automatically. The main program continues to run and prints a message. We wait a few seconds, and the program exits, terminating the daemon thread.

Then use the following command to run the script.

python daemon_thread_with_args.py

Of course, we can also set a thread as a daemon by calling the setDaemon(True) method on the thread instance. For example, Create a Project called daemon_thread_with_func.py in the WebIDE and enter the following content:

import threading
import time

## Define a function that runs indefinitely and prints messages at a regular interval
def my_function():
    while True:
        print("Hello from thread")
        time.sleep(1)

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

## Set the daemon flag to True so that the thread runs in the background and terminates when the main program exits
thread.setDaemon(True)

## Start the thread
thread.start()

## The main program continues to run and prints a message
print("Main program")

## Wait for a few seconds before exiting the program
time.sleep(5)

## The program exits and the daemon thread is terminated automatically
print("Main thread exiting...")

Executing the script with the following command will achieve the same result as the above example.

python daemon_thread_with_func.py

Event Object

In Python, you can use threading.Event object to allow threads to wait for a specific event to occur before proceeding. The Event object provides a way for one thread to signal that an event has occurred, and other threads can wait for that signal.

Create a Project called event_object.py in the WebIDE and enter the following content.

import threading

## Create an event object
event = threading.Event()

## Define a function that waits for the event to be set
def my_function():
    print("Waiting for event")
    ## Wait for the event to be set
    event.wait()
    print("Event received")

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

## Start the thread
thread.start()

## Signal the event after a few seconds
## The wait() call in the target function will now return and continue execution
event.set()

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

In this example, we create an Event object using the Event class. We define a function that waits for the event to be signaled using the wait method and then prints a message. We create a new thread and start it. After a few seconds, we signal the event using the set method. The thread receives the event and prints a message. Finally, we wait for the thread to finish using the join method.

Then use the following command to run the script.

python event_object.py

Timer Object

In Python, you can use threading.Timer object to schedule a function to run after a specific time has passed. The Timer object creates a new thread that waits for the specified time interval before executing the function.

Create a Project called timer_object.py in the WebIDE and enter the following content.

import threading

## Define a function to be executed by the Timer after 5 seconds
def my_function():
    print("Hello from timer")

## Create a timer that runs the target function after 5 seconds
timer = threading.Timer(5, my_function)

## Start the timer
timer.start()

## Wait for the timer to finish
timer.join()

In this example, we create a Timer object using the Timer class and pass it a time delay in seconds and a function to execute. We start the timer using the start method and wait for it to finish using the join method. After 5 seconds, the function is executed and prints a message.

Then use the following command to run the script.

python timer_object.py

Tips: The timer thread runs separately, so it may not be synchronized with the main thread. If your function relies on some shared state or resources, you will need to synchronize access to them appropriately. Also, remember that the timer thread will not stop the program from exiting if it is still running when all other non-daemon threads have been completed.

Barrier Object

In Python, you can use threading.Barrier object to synchronize multiple threads at predefined synchronization points. The Barrier object provides a way for a set of threads to wait for each other to reach a certain point in their execution before continuing.

Create a Project called barrier_object.py in the WebIDE and enter the following content.

import threading

## Create a Barrier object for 3 threads
barrier = threading.Barrier(3)

## Define a function that waits at the barrier
def my_function():
    print("Before barrier")
    ## Wait for all three threads to reach the barrier
    barrier.wait()
    print("After barrier")

## Create 3 threads using a loop and start them
threads = []
for i in range(3):
    thread = threading.Thread(target=my_function)
    threads.append(thread)
    thread.start()

## Wait for all threads to finish executing
for thread in threads:
    thread.join()

In this example, we create a Barrier object using the Barrier class and pass it the number of threads to wait for. Using the wait method, we define a function that waits for the barrier and prints a message. We create three threads and start them. Each thread waits for the barrier, so all threads will wait until all of them have reached the barrier. Finally, we wait for all threads to finish using the join method.

Then use the following command to run the script.

python barrier_object.py

Summary

That's it! You now know how to use the Python threading module in your code. It can help us to master the basic principles and techniques of concurrent programming in depth so that we can better develop efficient concurrent applications.

Other Python Tutorials you may like