Industrial manufacturing
Industrial Internet of Things | Industrial materials | Equipment Maintenance and Repair | Industrial programming |
home  MfgRobots >> Industrial manufacturing >  >> Industrial programming >> Python

Master Python Multithreading: GIL Explained with Practical Examples

Python offers both multiprocessing and multithreading to accelerate workloads. This guide walks you through creating robust, thread‑based applications, the nuances of the Global Interpreter Lock (GIL), and how to avoid common pitfalls like deadlocks and race conditions.

What Is a Thread?

A thread is a lightweight unit of execution within a single process. Threads share the process’s memory space, allowing efficient data exchange, while the operating system schedules them to run concurrently on one or more CPU cores.

What Is a Process?

A process is an independent program instance. When you launch an application—say a browser or text editor—the OS creates a separate process with its own code, data, and memory map.

Python Multithreading Fundamentals

Python’s threading module provides a high‑level API for managing threads, whereas the legacy _thread module is deprecated and only retained for backward compatibility. Threads are ideal for I/O‑bound tasks, while CPU‑bound work should use the multiprocessing module to bypass the GIL.

When to Use Multithreading

Multithreading shines when you need to perform several I/O‑heavy operations—network requests, file I/O, or database calls—simultaneously. Properly designed threads can improve responsiveness and throughput without the overhead of spawning new processes.

Key Python Modules for Threading

  1. _thread – Low‑level, deprecated.
  2. threading – Modern, feature‑rich, and the recommended choice.

Using the threading Module

The threading module offers a Thread class, synchronization primitives (Lock, RLock, Semaphore, Condition, Event, Barrier), and utility functions such as activeCount() and enumerate(). Below is a minimal example that demonstrates thread creation and synchronization.

import time
import _thread


def thread_test(name, wait):
    for i in range(4):
        time.sleep(wait)
        print(f"Running {name}")
    print(f"{name} has finished execution")

if __name__ == "__main__":
    _thread.start_new_thread(thread_test, ("First Thread", 1))
    _thread.start_new_thread(thread_test, ("Second Thread", 2))
    _thread.start_new_thread(thread_test, ("Third Thread", 3))

Running this code produces interleaved output that demonstrates concurrent execution.

Master Python Multithreading: GIL Explained with Practical Examples

Explanation

  1. Import time and the legacy _thread module.
  2. Define thread_test to perform work and log its progress.
  3. Launch three independent threads via start_new_thread.

Modern Threading with threading.Thread

In practice, you’ll subclass threading.Thread and override its run() method. The following example mirrors the previous behavior but uses the high‑level API.

import time
import threading

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

    def run(self):
        for _ in range(4):
            time.sleep(self.delay)
            print(f"Running {self.name}")
        print(f"{self.name} has finished execution")

if __name__ == "__main__":
    threads = [
        ThreadTester("First Thread", 1),
        ThreadTester("Second Thread", 2),
        ThreadTester("Third Thread", 3),
    ]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

Output:

Master Python Multithreading: GIL Explained with Practical Examples

Explanation

  1. Subclass Thread to encapsulate thread behavior.
  2. Override __init__ to store thread‑specific data.
  3. Override run to define the task.
  4. Instantiate, start, and join each thread.

Common Threading Pitfalls

Deadlocks

A deadlock occurs when two or more threads wait indefinitely for resources held by each other. Classic illustration: the Dining Philosophers problem—five philosophers compete for five forks, each needing two forks to eat. If every philosopher picks up the right fork first, a circular wait arises, halting all progress.

Race Conditions

When multiple threads modify shared data without proper coordination, the final state can be unpredictable. For example:

i = 0
for _ in range(100):
    print(i)
    i += 1

Launching this loop in parallel threads can produce a scrambled sequence because increments overlap.

Synchronizing Threads

The threading module supplies several synchronization primitives. The most fundamental is Lock, which ensures exclusive access to a shared resource.

import threading
lock = threading.Lock()


def worker(name):
    for _ in range(5):
        lock.acquire()
        print(f"{name} acquired lock")
        # critical section
        lock.release()

if __name__ == "__main__":
    t1 = threading.Thread(target=worker, args=("Thread A",))
    t2 = threading.Thread(target=worker, args=("Thread B",))
    t1.start(); t2.start()
    t1.join(); t2.join()

Output demonstrates serialized access to the critical section.

Master Python Multithreading: GIL Explained with Practical Examples

Other Synchronization Tools

Understanding the Global Interpreter Lock (GIL)

CPython uses the GIL to serialize bytecode execution across threads. While this protects the interpreter’s internal structures (e.g., reference counting) from race conditions, it also limits parallelism for CPU‑bound tasks on multi‑core systems.

  1. When a thread starts, it acquires the GIL.
  2. If another thread needs the GIL, it blocks until the first releases it.
  3. IO‑bound threads release the GIL during blocking calls, allowing others to run.
  4. CPU‑bound threads hold the GIL, preventing other threads from executing Python code.

Consequently, multithreading in Python yields little benefit for compute‑heavy workloads; multiprocessing is the preferred approach in those cases.

Why the GIL Exists

The CPython garbage collector relies on reference counting. Without a global lock, concurrent updates to reference counts would lead to race conditions and memory corruption. A per‑object lock would introduce significant overhead, so the GIL was adopted as a simple, effective solution.

Practical Takeaways

Python

  1. Java HashMap: A Comprehensive Guide
  2. Python OOP Fundamentals: Classes, Objects, Inheritance, and Constructors Explained
  3. Mastering Python’s strip() Method: Comprehensive Guide & Practical Examples
  4. Python Counter in collections – Efficient Counting, Updating, and Arithmetic Operations
  5. Creating ZIP Archives in Python: From Full Directory to Custom File Selection
  6. Master Python Unit Testing with PyUnit: A Practical Guide & Example
  7. Python List index() – How to Find Element Positions with Practical Examples
  8. Master Python Regular Expressions: re.match(), re.search(), re.findall() – Practical Examples
  9. Python Calendar Module: Expert Guide with Code Examples
  10. Master Python Attrs: Build Advanced Data Classes with Practical Examples