Python Threading Fundamentals and Concurrency Control

Creating Threads

Python's primary threading module is threading.

  • threading.current_thread(): Retrieves the currently active thread.
  • Thread objects have attributes like name and ident for identification.
  • Threads can be created using functions or by extending a class.

Function-Based Thread Creation

Pass the target function to threading.Thread().

import threading
import os
import time

def task_executor(delay):
    begin_time = time.perf_counter()
    current = threading.current_thread()
    print(f'{current.name} initiating...')
    time.sleep(delay)
    print(f'Thread: {current.name}, ID: {current.ident}, Process: {os.getpid()}')
    print(f'{current.name} finished after {time.perf_counter() - begin_time:.2f} seconds')

program_start = time.perf_counter()
active_threads = []

for idx in range(1, 4):
    thread = threading.Thread(target=task_executor, name=f'Worker-{idx}', args=(idx,))
    thread.start()
    active_threads.append(thread)

main_thread = threading.current_thread()
print(f'Main thread: {main_thread.name}, ID: {main_thread.ident}, Process: {os.getpid()}')
print(f"Total elapsed: {time.perf_counter() - program_start:.2f} seconds")

The output demonstrates that the main thread completes while child threads are still executing.

Class-Based Thread Creation

Extend the Thread class and override the run method.

import threading
import os
import time

class CustomThread(threading.Thread):
    def __init__(self, delay, name=None):
        super().__init__()
        self.name = name
        self.delay = delay

    def run(self):
        start = time.perf_counter()
        current = threading.current_thread()
        print(f'{current.name} starting...')
        time.sleep(self.delay)
        print(f'Thread: {current.name}, ID: {current.ident}, Process: {os.getpid()}')
        print(f'{current.name} completed in {time.perf_counter() - start:.2f} seconds')

program_start = time.perf_counter()
for i in range(1, 4):
    t = CustomThread(name=f'Task-{i}', delay=i)
    t.start()

main = threading.current_thread()
print(f'Main: {main.name}, ID: {main.ident}, Process: {os.getpid()}')
print(f"Total time: {time.perf_counter() - program_start:.2f} seconds")

Thread Control Mechanisms

Daemon Threads

Setting daemon=True makes threads terminate when the main thread ends.

import threading
import time

def background_task(duration):
    print(f'{threading.current_thread().name} running for {duration}s')
    time.sleep(duration)
    print(f'{threading.current_thread().name} done')

for i in range(3):
    t = threading.Thread(target=background_task, name=f'Daemon-{i}', args=(i+1,))
    t.daemon = True
    t.start()

time.sleep(1.5)
print('Main thread exiting')

Thread Joining

The join() method blocks the main thread until the target thread completes.

import threading
import time

def delayed_operation(seconds):
    print(f'Operation starting, will take {seconds}s')
    time.sleep(seconds)
    print('Operation complete')

thread_list = []
for i in range(3):
    t = threading.Thread(target=delayed_operation, args=(i+1,))
    t.start()
    thread_list.append(t)

for thread in thread_list:
    thread.join()

print('All threads have finished')

Synchronization Primitives

Basic Lock Mechanism

Shared resource access requires locking to prevent race conditions.

import threading
import time
import random

account_balance = 1000

def withdraw_funds(amount, lock_obj):
    global account_balance
    lock_obj.acquire()
    current = account_balance
    time.sleep(random.uniform(0, 0.1))
    current -= amount
    account_balance = current
    print(f'Withdrew {amount}, remaining: {account_balance}')
    lock_obj.release()

lock = threading.Lock()
threads = []
for i in range(5):
    t = threading.Thread(target=withdraw_funds, args=(100, lock))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print(f'Final balance: {account_balance}')

Reentrant Locks (RLock)

RLock allows a thread to acquire the same lock multiple times.

import threading

lock = threading.RLock()

def nested_lock_demo():
    lock.acquire()
    print('First lock acquired')
    lock.acquire()
    print('Second lock acquired')
    lock.release()
    print('First release')
    lock.release()
    print('Second release')

t1 = threading.Thread(target=nested_lock_demo)
t2 = threading.Thread(target=nested_lock_demo)
t1.start()
t2.start()
t1.join()
t2.join()

Thread Communication

Condition Variables

Condition objects enable thread coordination through wait/notify patterns.

import threading
import time

buffer = []
max_size = 5
condition = threading.Condition()

def producer():
    global buffer
    count = 0
    while True:
        with condition:
            while len(buffer) >= max_size:
                condition.wait()
            item = f'Item-{count}'
            buffer.append(item)
            print(f'Produced: {item}')
            count += 1
            condition.notify()
        time.sleep(0.5)

def consumer():
    global buffer
    while True:
        with condition:
            while not buffer:
                condition.wait()
            item = buffer.pop(0)
            print(f'Consumed: {item}')
            condition.notify()
        time.sleep(1)

p = threading.Thread(target=producer)
c = threading.Thread(target=consumer)
p.daemon = True
c.daemon = True
p.start()
c.start()
time.sleep(3)

Semaphores

Semaphores limit concurrent access to resources.

from threading import Thread, Semaphore
import time

MAX_CONCURRENT = 3
resource_sem = Semaphore(MAX_CONCURRENT)

def access_resource(thread_id):
    with resource_sem:
        print(f'Thread {thread_id} accessing resource')
        time.sleep(2)
        print(f'Thread {thread_id} releasing resource')

threads = [Thread(target=access_resource, args=(i,)) for i in range(10)]
for t in threads:
    t.start()
for t in threads:
    t.join()

Event Objects

Events provide simple signaling betweeen threads.

from threading import Thread, Event
import time

signal = Event()

def signal_waiter(waiter_id):
    print(f'Waiter {waiter_id} waiting')
    signal.wait()
    print(f'Waiter {waiter_id} proceeding')

def signal_setter():
    time.sleep(2)
    print('Setting signal')
    signal.set()

Thread(target=signal_setter).start()
for i in range(3):
    Thread(target=signal_waiter, args=(i,)).start()

Thread Pool Execution

The concurrent.futures module provides high-level thread pooling.

from concurrent.futures import ThreadPoolExecutor, as_completed
import time

def process_item(item_id):
    time.sleep(item_id * 0.5)
    return f'Processed-{item_id}'

with ThreadPoolExecutor(max_workers=4) as executor:
    futures = [executor.submit(process_item, i) for i in range(8)]
    for future in as_completed(futures):
        print(future.result())

Additional pool methods include map() for ordered results and wait() for synchronization.

Tags: python threading multithreading Concurrency ThreadPoolExecutor

Posted on Sat, 24 Oct 2026 16:29:32 +0000 by azsolo