iph.ar
  • home
  • Misc.
  • Gaming
  • (de)Engineering
    • Using LLMs to Simulate Wild Thinking and Convergence
    • Visualizing Song Structure with Self-Similarity Matrices
    • PyHPC – Self-Guided Parallel Programming Workshop with Python
      • 00 - How This Course Was Made
      • 01 - Multiprocessing
        • 1.1 - HPC landscape overview
        • 1.2 - Introduction to the multiprocessing module
        • 1.3 - Using Process, Queue, Pipe, and Pool
        • 1.4 - Introduction to concurrent.futures
        • 1.5 - Examples of CPU-bound tasks with performance profiling
        • 1.6 - Comparison with sequential execution
        • 1.7 - Synchronization and locking considerations
        • 1.8 - Profiling CPU-bound vs I/O-bound tasks
        • Solution: Parallel prime number calculation
        • Solution: Signal filtering comparison
      • 02 - Multithreading and GIL
      • 03 - MPI with mpi4py
      • 04 - GPU with PyCUDA/Numba
      • 05 - Parallel Libraries
    • Mapping Consonants as Percussion: A Small Experiment with Whisper and Audio Analysis
    • Semantic Self-Similarity or How I Split a Conversation into Scenes Using Language Models
    • Modeling the Noise: Building a Tinnitus Generator in Python
    • Making A Humble OpenGL Rotating Cube
  • IT
  • home
  • Misc.
  • Gaming
  • (de)Engineering
    • Using LLMs to Simulate Wild Thinking and Convergence
    • Visualizing Song Structure with Self-Similarity Matrices
    • PyHPC – Self-Guided Parallel Programming Workshop with Python
      • 00 - How This Course Was Made
      • 01 - Multiprocessing
        • 1.1 - HPC landscape overview
        • 1.2 - Introduction to the multiprocessing module
        • 1.3 - Using Process, Queue, Pipe, and Pool
        • 1.4 - Introduction to concurrent.futures
        • 1.5 - Examples of CPU-bound tasks with performance profiling
        • 1.6 - Comparison with sequential execution
        • 1.7 - Synchronization and locking considerations
        • 1.8 - Profiling CPU-bound vs I/O-bound tasks
        • Solution: Parallel prime number calculation
        • Solution: Signal filtering comparison
      • 02 - Multithreading and GIL
      • 03 - MPI with mpi4py
      • 04 - GPU with PyCUDA/Numba
      • 05 - Parallel Libraries
    • Mapping Consonants as Percussion: A Small Experiment with Whisper and Audio Analysis
    • Semantic Self-Similarity or How I Split a Conversation into Scenes Using Language Models
    • Modeling the Noise: Building a Tinnitus Generator in Python
    • Making A Humble OpenGL Rotating Cube
  • IT

1.4 - Introduction to `concurrent.futures`

September 2025

Key Concept

In Python, executing tasks in parallel can significantly improve program performance, especially for computationally intensive operations. However, managing threads and processes directly can be complex and error-prone. The `concurrent.futures` module provides a high-level interface for asynchronously executing callables, making parallel programming much cleaner and easier to manage. This section introduces the core concepts and functionalities of `concurrent.futures`, highlighting how it simplifies parallel execution.

Topics

Executor: The core object that manages the execution of tasks. Choose from `ThreadPoolExecutor` (for I/O-bound tasks) or `ProcessPoolExecutor` (for CPU-bound tasks).

At the heart of `concurrent.futures` is the `Executor` class. Think of an `Executor` as a factory that creates and manages worker threads or processes. It takes a callable (a function or method) and submits it to the worker for execution. The `Executor` handles the details of scheduling the task and retrieving the result. It abstracts away the complexities of thread or process management, allowing you to focus on the task itself. You choose the type of `Executor` (e.g., `ThreadPoolExecutor` or `ProcessPoolExecutor`) based on the nature of your task – whether it's I/O bound or CPU bound.

submit():  A method to submit a callable to the executor, returning a `Future` object.

The `submit()` method is the primary way to submit a callable to the `Executor`. It takes the callable as an argument and returns a `Future` object. The `submit()` method doesn't block; it immediately returns the `Future` object, allowing the main program to continue executing while the task is being processed by the worker. The `Future` object acts as a placeholder for the eventual result of the task. You can later use the `Future` object to check if the task is complete, retrieve the result, or handle any exceptions that occurred during execution.

Future: Represents the result of an asynchronous computation. Use `Future.result()` to retrieve the result (blocking until it's available).

A `Future` object represents the result of an asynchronous computation. It's a placeholder for a value that will be available in the future. You can use the `Future` object to:

  • Check for Completion:  Use the `done()` method to determine if the task has finished.
    • Retrieve the Result: Use the `result()` method to retrieve the result of the task. If the task is not yet complete, `result()` will block until the result is available.
      • Handle Exceptions: Use the `exception()` method to retrieve any exception that occurred during the task's execution.
        • Cancel the Task: Use the `cancel()` method to attempt to cancel the task. Note that cancellation is not always guaranteed to be successful.

          Asynchronous Execution: Tasks run concurrently, allowing for faster overall execution time.

          `concurrent.futures` enables asynchronous execution, meaning that tasks can run concurrently without blocking the main program's execution flow. This is particularly beneficial for I/O-bound tasks, where the program spends a significant amount of time waiting for external operations (e.g., network requests, file reads). By submitting tasks to an `Executor`, the main program can continue to execute other tasks while the worker threads or processes handle the asynchronous operations. This significantly improves overall program responsiveness and throughput.

          Again, the following subsections were not planned but were generated by the model during expansion. They reflect related HPC topics that go beyond the scope of the original outline. We keep them here as an example of LLM verbosity rampage.

          ThreadPoolExecutor

          The `ThreadPoolExecutor` is designed for I/O-bound tasks. It creates a pool of worker threads that can execute tasks concurrently. Threads are lightweight and efficient for handling tasks that spend a lot of time waiting for external operations. `ThreadPoolExecutor` is suitable when the tasks are primarily waiting for I/O and don't require significant CPU power. It's a good choice when you have many independent tasks that can be executed concurrently without significant data dependencies.

          ProcessPoolExecutor

          The `ProcessPoolExecutor` is designed for CPU-bound tasks. It creates a pool of worker processes, each with its own memory space. This allows for true parallelism, as each process can execute tasks independently without being limited by the Global Interpreter Lock (GIL) in Python. The GIL restricts the ability of multiple threads to execute Python bytecode concurrently. `ProcessPoolExecutor` is ideal for tasks that require significant CPU power, such as numerical computations, image processing, or data analysis. However, creating and managing processes is more resource-intensive than creating and managing threads.

          Exercise

          Consider a simple function that performs a computationally intensive calculation. How could you use `concurrent.futures` to run this calculation in parallel with another task (e.g., reading data from a file)? (No code required, just conceptualize the flow).

          Answer: Imagine you’re querying a NoSQL database to fetch documents. That I/O-bound task could run in one thread, while other cores are busy crunching the retrieved data (e.g., aggregating statistics, filtering images, training a small model). Using concurrent.futures, you’d schedule the query as one future and the CPU-heavy computations as others, letting them proceed in parallel instead of waiting for the query to finish first.

          💡 Pitfalls

          • Resource Contention:  Be mindful of shared resources (e.g., files, databases) when using multiple processes or threads.
            • Overhead: Parallelization isn't always faster; the overhead of managing concurrency can sometimes outweigh the benefits, especially for very short tasks.


              💡 Best Practices

              • Choose the Right Executor: Select `ThreadPoolExecutor` for I/O-bound tasks and `ProcessPoolExecutor` for CPU-bound tasks.
                • Handle Exceptions: Wrap your callable in a `try...except` block to gracefully handle exceptions that might occur during execution.


                  ⟵ 1.3 - Using `Process`, `Queue`, `Pipe`, and `Pool` 1.5 - Examples of CPU-bound tasks with performance profiling ⟶
                  • © 2025 iph.ar