Multithreading in Python 3

Multithreading is a powerful feature in Python that allows multiple threads to run concurrently within a single process. This is particularly useful for I/O-bound tasks, such as network requests or file operations, where threads can perform other operations while waiting for I/O to complete.

What is a Thread?

A thread is a lightweight, independent sequence of execution within a program. Threads in the same process share memory space, which facilitates easier communication and data sharing compared to separate processes.

Global Interpreter Lock (GIL)

Python’s GIL is a mechanism that prevents multiple native threads from executing Python bytecodes simultaneously. This means that even on multi-core systems, only one thread can execute at a time in CPython, the standard Python implementation. However, threads can still be useful for I/O-bound tasks where they can wait for external resources without blocking the entire program.

Advantages of Multithreading

  • Responsiveness: Applications remain responsive while performing lengthy operations.
  • Resource Sharing: Threads share the same memory space, which means it is easy to share data.
  • Simplified Code Structure: Multithreading can simplify the handling of concurrent tasks.

Using the threading Module

The threading module provides a high-level interface for creating and managing threads. Here’s how to use it effectively:

  1. Import the Module:
import threading

2. Define Functions for Threads:

These functions will be executed by the threads.

def print_square(num):
    print(f"Square: {num * num}")

def print_cube(num):
    print(f"Cube: {num * num * num}")

3. Create Thread Objects:

Each thread is instantiated from the Thread class.

t1 = threading.Thread(target=print_square, args=(10,))
t2 = threading.Thread(target=print_cube, args=(10,))

4. Start the Threads:

This begins the execution of the threads.

t1.start()
t2.start()

5. Wait for Threads to Complete:

Use join() to ensure that the main program waits for the threads to finish.

t1.join()
t2.join()
print("Done!")

Example 1: Basic Multithreading

import threading

def print_square(num):
    print(f"Square: {num * num}")

def print_cube(num):
    print(f"Cube: {num * num * num}")

if __name__ == "__main__":
    # Create threads
    t1 = threading.Thread(target=print_square, args=(10,))
    t2 = threading.Thread(target=print_cube, args=(10,))
    
    # Start threads
    t1.start()
    t2.start()
    
    # Wait for both threads to complete
    t1.join()
    t2.join()
    
    print("Done!")

Output:

Square: 100
Cube: 1000
Done!

Note: The order of “Square” and “Cube” may vary because both threads run concurrently. You might see “Cube” printed before “Square” or vice versa.

Example 2: Using Multiple Threads with a Queue

import threading
import queue

# Function to calculate square and cube
def calculate_square_and_cube(q):
    while not q.empty():
        num = q.get()
        print(f"Square of {num}: {num * num}")
        print(f"Cube of {num}: {num * num * num}")
        q.task_done()

if __name__ == "__main__":
    # Create a queue and add numbers to it
    q = queue.Queue()
    numbers = [1, 2, 3, 4, 5]

    for number in numbers:
        q.put(number)

    # Create multiple threads
    threads = []
    for _ in range(3):  # Create three threads
        thread = threading.Thread(target=calculate_square_and_cube, args=(q,))
        thread.start()
        threads.append(thread)

    # Wait for all tasks in the queue to be processed
    q.join()

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

    print("All calculations done!")

Output:

Square of 1: 1
Cube of 1: 1
Square of 2: 4
Cube of 2: 8
Square of 3: 9
Cube of 3: 27
Square of 4: 16
Cube of 4: 64
Square of 5: 25
Cube of 5: 125
All calculations done!

Note: The order of the output for squares and cubes may vary due to the concurrent execution of multiple threads. Each thread picks numbers from the queue and processes them independently, so the output may not follow a strictly sequential order.

Explanation:

  • Queue: Tasks are handled with a thread-safe queue. In this queue, each thread finds a number; it then squares and cubes this number and then marks the task as complete.
  • Multiple Threads: Three threads are created to process numbers concurrently. This allows multiple calculations to happen simultaneously.

Conclusion

Multithreading in Python is an important technique that helps improve application performance and responsiveness, especially for I/O-bound tasks. Effective use of the threading module and understanding concepts like GIL and shared memory space will help developers build efficient multithreaded applications that handle concurrent operations seamlessly.