Threading Fundamentals in Python: Core Concepts and Implementation Examples

Threads: A Conceptual Overview

A thread is a lightweight execution path within a program and represents the smallest unit of processing managed by an operating system scheduler. Threads share the resources of their parent process but maintain minimal independent resources essantial for execution.

Main Thread

When a Python program launches, the operating system creates a process. A primary thread of execution, known as the main thread, starts immediately. This thread is significant because it spawns child threads and must typically be the last to terminate to handle cleanup operations.

Child Threads

A child thread represents an alternate execution branch that runs concurrently with the main thread after initiation.

Sequential vs. Concurrent Execution Eaxmples

Sequential (Single-Threaded) Example:

import time

def display_message():
    print("Message displayed")
    time.sleep(1)

if __name__ == '__main__':
    for _ in range(5):
        display_message()

Execution time is approximately 5 seconds.

Concurrent (Multi-Threaded) Example:

import time
import threading

def display_message():
    print("Message displayed")
    time.sleep(1)

if __name__ == '__main__':
    for _ in range(5):
        thread_instance = threading.Thread(target=display_message)
        thread_instance.start()

Execution time reduces to approximately 1 second.

Main Thread Waits for Child Thread Completion

The main thread remains active until all child threads have finished execution.

import time
import threading

def vocalize():
    for count in range(3):
        print(f"Vocalizing iteration {count}")
        time.sleep(1)

def move():
    for count in range(3):
        print(f"Moving iteration {count}")
        time.sleep(1)

if __name__ == '__main__':
    print("Main thread begins execution")
    
    vocal_thread = threading.Thread(target=vocalize)
    move_thread = threading.Thread(target=move)
    
    vocal_thread.start()
    move_thread.start()
    
    print("Main thread execution concludes")

Monitoring Active Thread Count

Use threading.enumerate() to obtain a list of all currently active thread objects and len() to count them.

import time
import threading

def vocalize():
    for count in range(3):
        print(f"Vocalizing iteration {count}")
        time.sleep(1)

def move():
    for count in range(3):
        print(f"Moving iteration {count}")
        time.sleep(1)

if __name__ == '__main__':
    active_threads = threading.enumerate()
    print(f"Initial thread count: {len(active_threads)}")
    
    vocal_thread = threading.Thread(target=vocalize)
    move_thread = threading.Thread(target=move)
    
    vocal_thread.start()
    move_thread.start()
    
    active_threads = threading.enumerate()
    print(f"Updated thread count: {len(active_threads)}")

Passing Arguments to Threads

Three methods exist for passing arguments to thread functions.

1. Tuple Arguments

threading.Thread(target=function_name, args=(arg1, arg2, ...))

import time
import threading

def process_values(x, y, z):
    print(f"Values: x={x}, y={y}, z={z}")
    time.sleep(1)

if __name__ == '__main__':
    for _ in range(5):
        thread_instance = threading.Thread(target=process_values, args=(10, 20, 30))
        thread_instance.start()

2. Dictionary Arguments

threading.Thread(target=function_name, kwargs={"param1": value1, "param2": value2, ...})

import time
import threading

def process_values(x, y, z):
    print(f"Values: x={x}, y={y}, z={z}")
    time.sleep(1)

if __name__ == '__main__':
    for _ in range(5):
        thread_instance = threading.Thread(target=process_values, kwargs={"x": 10, "z": 20, "y": 30})
        thread_instance.start()

3. Combined Tuple and Dictionary Arguments

threading.Thread(target=function_name, args=(arg1,), kwargs={"param2": value2, ...})

import time
import threading

def process_values(x, y, z):
    print(f"Values: x={x}, y={y}, z={z}")
    time.sleep(1)

if __name__ == '__main__':
    for _ in range(5):
        thread_instance = threading.Thread(target=process_values, args=(10,), kwargs={"z": 20, "y": 30})
        thread_instance.start()

Daemon Threads

A daemon thread terminates automatically when the main thread exits. Configure by setting thread_instance.daemon = True before starting the thread (default is False).

import time
import threading

def repetitive_output(a, b, c):
    for iteration in range(5):
        print(f"Current output: {iteration}")
        time.sleep(1)

if __name__ == '__main__':
    thread_instance = threading.Thread(target=repetitive_output, args=(10,), kwargs={"c": 20, "b": 30})
    thread_instance.daemon = True
    thread_instance.start()
    
    time.sleep(1)
    print("Main thread terminating...")
    exit()

Concurrency vs. Parallelism

Multi-Tasking Concept

Multi-tasking involves the operating system rapidly switching between tasks. Each task receives a small time slice (e.g., 0.01 seconds) before switching to the next. The high switching speed creates the illusion of simultaneous execution.

Definitions

  • Concurrency: Occurs when the number of tasks exceeds CPU cores. The operating system scheduler gives the appearance of simultaneous execution through rapid task switching.
  • Parallelism: Occurs when the number of tasks is less than or equal to CPU cores, enabling genuine simultaneous execution.

True parallelism requires a multi-core CPU. However, with many more tasks then cores, the operating system schedules tasks across all available cores.

Creating Custom Thread Classes

For better encapsulation, you can define custom thread classes by extending threading.Thread.

Implementation steps:

  1. Inherit from threading.Thread
  2. Override the run() method
  3. Instantiate the class and call start()
import threading
import time

class CustomThread(threading.Thread):
    def __init__(self, iterations):
        super().__init__()  # Required parent class initialization
        self.iterations = iterations
    
    def run(self):
        for i in range(self.iterations):
            print(f"Executing run method in custom thread: {i}")
            time.sleep(0.5)

if __name__ == '__main__':
    custom_thread = CustomThread(5)
    custom_thread.start()

Global Variable Sharing Among Threads

Multiple threads within the same process share access to global variables.

import threading
import time

shared_counter = 0

def increment_counter():
    global shared_counter
    for _ in range(10):
        shared_counter += 1
        time.sleep(0.5)
        print(f"Thread 1 counter: {shared_counter}")

def read_counter():
    for _ in range(10):
        time.sleep(0.5)
        print(f"Thread 2 counter: {shared_counter}")

if __name__ == '__main__':
    t1 = threading.Thread(target=increment_counter)
    t2 = threading.Thread(target=read_counter)
    
    t1.start()
    t2.start()
    
    while len(threading.enumerate()) > 1:
        time.sleep(1)
    print(f"Final counter value: {shared_counter}")

Issues with Shared Variables

Race conditions can occur when multiple threads modify a shared variable concurrently without proper synchronization.

import threading

shared_counter = 0

def worker_one():
    global shared_counter
    for _ in range(1000000):
        shared_counter += 1
    print(f"Worker one result: {shared_counter}")

def worker_two():
    global shared_counter
    for _ in range(1000000):
        shared_counter += 1
    print(f"Worker two result: {shared_counter}")

if __name__ == '__main__':
    t1 = threading.Thread(target=worker_one)
    t2 = threading.Thread(target=worker_two)
    
    t1.start()
    t2.start()
    
    while len(threading.enumerate()) > 1:
        time.sleep(1)
    print(f"Main thread result: {shared_counter}")

Solution Using join()

The join() method forces sequential execution, converting concurrent operations to sequential ones.

import threading

shared_counter = 0

def worker_one():
    global shared_counter
    for _ in range(1000000):
        shared_counter += 1
    print(f"Worker one result: {shared_counter}")

def worker_two():
    global shared_counter
    for _ in range(1000000):
        shared_counter += 1
    print(f"Worker two result: {shared_counter}")

if __name__ == '__main__':
    t1 = threading.Thread(target=worker_one)
    t2 = threading.Thread(target=worker_two)
    
    t1.start()
    t1.join()  # Wait for t1 to complete
    t2.start()
    
    while len(threading.enumerate()) > 1:
        time.sleep(1)
    print(f"Main thread result: {shared_counter}")

Synchronous vs. Asynchronous Operations

  • Synchronous: Tasks execute in a specific sequence, where one must complete before another begins. Only one primary execution flow exists (e.g., a conversation where one speaker talks at a time).
  • Asynchronous: Tasks execute independently without sequencing constraints, allowing multiple concurrent execution flows (e.g., sending multiple messages without waiting for responses).

Mutex Locks for Synchronization

A mutex (mutual exclusion) lock provides synchronization for shared resources. A lock has two states: locked and unlocked. When a thread locks a resource, other threads cannot modify it until it's unlocked.

import threading
import time

shared_counter = 0
lock = threading.Lock()

def increment_with_lock():
    global shared_counter
    lock.acquire()
    for _ in range(1000000):
        shared_counter += 1
    lock.release()
    print(f"Thread 1 final value: {shared_counter}")

def another_increment_with_lock():
    global shared_counter
    lock.acquire()
    for _ in range(1000000):
        shared_counter += 1
    lock.release()
    print(f"Thread 2 final value: {shared_counter}")

if __name__ == '__main__':
    t1 = threading.Thread(target=increment_with_lock)
    t2 = threading.Thread(target=another_increment_with_lock)
    
    t1.start()
    t2.start()
    
    while len(threading.enumerate()) > 1:
        time.sleep(1)
    print(f"Final counter value: {shared_counter}")

Deadlocks

A deadlock occurs when two or more threads hold resources while waiting for resources held by other threads, creating a circular dependency.

Example of a potential deadlock:

import threading

def access_element(idx):
    numbers = [1, 3, 5, 7, 9]
    lock.acquire()
    
    if idx >= len(numbers):
        print(f"Index {idx} out of bounds")
        return  # Missing lock.release() causes deadlock
    
    print(numbers[idx])
    lock.release()

if __name__ == '__main__':
    lock = threading.Lock()
    
    for i in range(10):
        t = threading.Thread(target=access_element, args=(i,))
        t.start()

Solution: Ensure lock release before returning.

import threading

def access_element(idx):
    numbers = [1, 3, 5, 7, 9]
    lock.acquire()
    
    if idx >= len(numbers):
        print(f"Index {idx} out of bounds")
        lock.release()  # Release lock before returning
        return
    
    print(numbers[idx])
    lock.release()

if __name__ == '__main__':
    lock = threading.Lock()
    
    for i in range(10):
        t = threading.Thread(target=access_element, args=(i,))
        t.start()

Tags: python threading Concurrency multithreading programming

Posted on Fri, 25 Sep 2026 16:12:19 +0000 by revbackup