Python Threading Basics

Threading in Python is a technique used to execute multiple threads (smaller units of a program) concurrently within a single process. It enables parallelism and can be beneficial for tasks that are I/O-bound or where operations can be carried out independently.

Basic Threading Example

import threading def print_numbers(): for i in range(1, 6): print("Number:", i) def print_letters(): for letter in 'abcde': print("Letter:", letter) if __name__ == "__main__": t1 = threading.Thread(target=print_numbers) t2 = threading.Thread(target=print_letters) t1.start() t2.start() t1.join() t2.join() print("Both threads have finished")

In this example, two functions, print_numbers() and print_letters(), are defined to print numbers and letters respectively. Two threads are created, one for each function. The start() method initiates each thread's execution, and join() ensures that the main program waits for both threads to finish before proceeding.

Thread Synchronization with Locks

Threads can access shared resources concurrently, potentially leading to synchronization issues. Locks can be used to prevent multiple threads from accessing a resource simultaneously.

counter = 0 counter_lock = threading.Lock() def increment_counter(): global counter for _ in range(100000): with counter_lock: counter += 1 if __name__ == "__main__": t1 = threading.Thread(target=increment_counter) t2 = threading.Thread(target=increment_counter) t1.start() t2.start() t1.join() t2.join() print("Final counter value:", counter)

In this example, a global counter is incremented by two threads. To prevent race conditions, a lock (counter_lock) is used within a context manager to ensure only one thread can modify the counter at a time.

ThreadPoolExecutor for Concurrent Execution

Python's concurrent.futures module provides a higher-level interface for managing threads and processes. The ThreadPoolExecutor class simplifies thread management for executing functions concurrently.

import concurrent.futures def process_data(data): return data * 2 if __name__ == "__main__": data_list = [1, 2, 3, 4, 5] with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: results = executor.map(process_data, data_list) for result in results: print("Processed data:", result)

In this example, a list of data is processed using the process_data() function. The ThreadPoolExecutor is used to concurrently execute the function on each item in the list, distributing the workload among the specified number of worker threads.

Conclusion

Threading in Python can greatly improve the efficiency of programs that involve I/O-bound operations or parallelizable tasks, but care must be taken to manage shared resources and potential synchronization issues.