Multithreading in Python

Loading

Multithreading is the ability of a CPU (or a single core) to provide multiple threads of execution concurrently, executing multiple tasks in parallel. In Python, multithreading can be used to perform multiple tasks at the same time within a single process, improving efficiency for I/O-bound tasks, such as file reading, network requests, or database queries.

Key Points:

  • Thread: A thread is the smallest unit of a CPU’s execution, consisting of a sequence of instructions.
  • Multithreading: Running multiple threads concurrently within a process.
  • Threading module: Python’s threading module allows the creation and management of threads.

2. The Global Interpreter Lock (GIL)

Python uses the Global Interpreter Lock (GIL), which means that only one thread can execute Python bytecode at a time in a single process. This can make multithreading less effective for CPU-bound tasks, such as heavy calculations or complex algorithms.

However, multithreading is still beneficial for I/O-bound tasks (like reading/writing files, web scraping, or database queries) because the GIL is released during I/O operations, allowing other threads to run while waiting for I/O operations to complete.


3. The threading Module

The threading module provides all the tools to create and manage threads in Python.

Basic Components of the threading Module:

  • Thread class: Allows creating new threads.
  • Lock/Mutex: Used to prevent multiple threads from accessing the same resource simultaneously, avoiding data corruption.
  • Event: Provides a way for one thread to notify others that an event has occurred.

4. Creating and Starting Threads

You can create threads by creating instances of the Thread class and passing a target function to run in the new thread.

Example: Creating Threads

import threading
import time

# Function to be run by each thread
def print_numbers():
for i in range(5):
print(i)
time.sleep(1) # Simulate work

# Create threads
thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_numbers)

# Start threads
thread1.start()
thread2.start()

# Wait for threads to finish
thread1.join()
thread2.join()

print("Both threads have finished.")

Explanation:

  • threading.Thread(target=print_numbers) creates a new thread that will execute the print_numbers() function.
  • start() begins the thread’s execution.
  • join() ensures the main thread waits for the completion of both threads before proceeding.

5. Using Locks to Prevent Race Conditions

When multiple threads access shared resources, such as a variable or a file, it may cause race conditions (where the result depends on the order of thread execution). You can use locks to prevent this.

Example: Using Locks

import threading

# Shared resource
counter = 0

# Lock object
lock = threading.Lock()

# Function that increments the counter
def increment():
global counter
with lock: # Acquire the lock
counter += 1
print(f"Counter: {counter}")

# Create multiple threads
threads = [threading.Thread(target=increment) for _ in range(5)]

# Start threads
for thread in threads:
thread.start()

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

print("Final Counter:", counter)

Explanation:

  • lock = threading.Lock() creates a lock object.
  • with lock: ensures that only one thread can access the critical section of code at a time (the increment of the counter variable).

6. Daemon Threads

Daemon threads are threads that run in the background and terminate when the main program ends, even if they haven’t finished their execution. They are typically used for tasks like logging, monitoring, or background cleanup.

Example: Daemon Threads

import threading
import time

def background_task():
while True:
print("Background task running...")
time.sleep(1)

# Create a daemon thread
daemon_thread = threading.Thread(target=background_task, daemon=True)

# Start the daemon thread
daemon_thread.start()

# Main program execution
time.sleep(5)
print("Main program ends.")

Explanation:

  • daemon=True sets the thread as a daemon.
  • The daemon thread will keep running until the main program ends, and then it will automatically terminate.

7. Thread Synchronization Using Events

In multithreading, sometimes you need one thread to wait for another thread to reach a certain point before proceeding. This can be done using events.

Example: Using Event for Synchronization

import threading
import time

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

def task1():
print("Task 1 starting.")
time.sleep(2)
print("Task 1 done.")
event.set() # Signal that task1 is done

def task2():
print("Task 2 waiting for event.")
event.wait() # Wait for task1 to signal
print("Task 2 starting after task 1.")

# Create threads
thread1 = threading.Thread(target=task1)
thread2 = threading.Thread(target=task2)

# Start threads
thread1.start()
thread2.start()

# Wait for threads to finish
thread1.join()
thread2.join()

print("Both tasks completed.")

Explanation:

  • event.set() signals the event, allowing other threads that are waiting on this event (using event.wait()) to proceed.

Leave a Reply

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