Threading in Python

0 0
Read Time:3 Minute, 27 Second

Mastering Threading in Python

Threads in Python allow concurrent execution of different parts of a program, enabling it to perform multiple operations at once. Here are some notes and an example for beginners:

Python’s threading module allows you to create and manage threads. Threads are lighter than processes, making them suitable for tasks like I/O-bound operations where the program spends a lot of time waiting for external events.

Basic Concepts:

  1. Thread: A sequence of instructions within a program that can be executed independently.
  2. Concurrency: Multiple threads executing seemingly simultaneously.
  3. Global Interpreter Lock (GIL): In CPython, the standard implementation of Python, the GIL allows only one thread to execute Python bytecode at a time, although I/O-bound tasks can still benefit from threading.
  4. Locks: Used to prevent multiple threads from accessing shared resources simultaneously to avoid conflicts (e.g., threading.Lock()).

Example:

Let’s create a simple example to illustrate threading in Python. Here’s a script that runs two threads concurrently, each printing numbers in a range:

import threading

def print_numbers(start, end):
    for i in range(start, end):
        print(i)

# Define the ranges
range1 = (1, 6)
range2 = (7, 12)

# Create threads
thread1 = threading.Thread(target=print_numbers, args=range1)
thread2 = threading.Thread(target=print_numbers, args=range2)

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

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

print("Threads finished.")

Notes:

  • threading.Thread(target=function, args=args_tuple): Creates a new thread.
  • start(): Initiates the thread’s execution.
  • join(): Waits for the thread to finish before moving forward in the main program.
  • Shared resources accessed by multiple threads should be properly synchronized using locks to avoid race conditions.

Important Tips:

  • GIL: In CPython, the GIL can limit the effectiveness of threading for CPU-bound tasks. For such tasks, consider using the multiprocessing module, which bypasses the GIL by using separate processes instead of threads.
  • Avoid Global Variables: Shared data among threads can lead to issues. Use locks to control access to shared resources.

Remember, threading in Python might not always provide the expected performance boost due to the GIL, but it’s still useful for I/O-bound tasks or situations involving concurrent operations. For CPU-bound tasks, consider using the multiprocessing module.

Start experimenting with these basics, and gradually delve deeper into more advanced threading concepts and scenarios!

Here are a few more examples of threading in Python showcasing different use cases:

1. Threaded Download Manager

import threading
import requests

def download_file(url, filename):
    response = requests.get(url)
    with open(filename, 'wb') as file:
        file.write(response.content)
    print(f"Downloaded {filename} from {url}")

urls = [
    ("https://example.com/file1.pdf", "file1.pdf"),
    ("https://example.com/image.jpg", "image.jpg"),
    # Add more URLs and filenames as needed
]

threads = []
for url, filename in urls:
    thread = threading.Thread(target=download_file, args=(url, filename))
    thread.start()
    threads.append(thread)

for thread in threads:
    thread.join()

print("All downloads finished.")

2. Threaded Task Execution with Function Arguments

import threading

def task_with_args(arg1, arg2):
    print(f"Received arguments: {arg1}, {arg2}")

arguments = [
    ("Hello", "World"),
    (42, [1, 2, 3]),
    # Add more argument pairs as needed
]

threads = []
for args in arguments:
    thread = threading.Thread(target=task_with_args, args=args)
    thread.start()
    threads.append(thread)

for thread in threads:
    thread.join()

print("All tasks finished.")

3. Thread Pooling with concurrent.futures

import concurrent.futures

def square_number(num):
    return num * num

numbers = [1, 2, 3, 4, 5]

with concurrent.futures.ThreadPoolExecutor() as executor:
    results = executor.map(square_number, numbers)

print(list(results))  # Output: [1, 4, 9, 16, 25]

4. Synchronized Access with Locks

import threading

counter = 0
lock = threading.Lock()

def increment_counter():
    global counter
    for _ in range(100000):
        with lock:
            counter += 1

threads = []
for _ in range(10):
    thread = threading.Thread(target=increment_counter)
    thread.start()
    threads.append(thread)

for thread in threads:
    thread.join()

print(f"Final counter value: {counter}")  # Output: 1000000

These examples illustrate different threading scenarios like parallel downloads, executing functions with arguments, utilizing thread pools, and managing shared resources using locks. Always ensure proper synchronization and error handling when working with threads to avoid potential issues like race conditions and deadlocks.

Happy
Happy
0 %
Sad
Sad
0 %
Excited
Excited
0 %
Sleepy
Sleepy
0 %
Angry
Angry
0 %
Surprise
Surprise
0 %