sodcalculator

10 Tech How-To

Return Thread Pool Results in Submission Order

Guide 10-32982 · Updated · Step by step

When you submit tasks to a Python ThreadPoolExecutor, the results come back in the order the tasks finish, not the order you submitted them. If your application needs the outputs aligned with the inputs—for example, when processing rows of a file or making parallel API calls whose responses you must map back—you need a way to preserve submission order. This guide shows you three reliable methods: using as_completed with a mapping, using map(), and using a custom index. You’ll also learn the pitfalls and how to debug common issues.

What You’ll Need

  • Python 3.6 or later (the concurrent.futures module is in the standard library).
  • Basic familiarity with functions, lists, and the with statement.
  • No third-party packages are required.

Step-by-Step

  1. Import the necessary modules. You need ThreadPoolExecutor and as_completed from concurrent.futures. Optionally, import time if you want to simulate variable‑length tasks.
  2. Define a worker function. This function will be executed by the thread pool. It should accept one argument (the task input) and return a result. For testing, you can use a function that sleeps for a random fraction of a second to simulate real work.
  3. Create a list of inputs. These are the items you want to process. The order of this list is the submission order you want to preserve.
  4. Method 1: Use map() (simplest). The executor.map() method returns an iterator that yields results in the exact order of the input iterable. It blocks until the next result is ready, so if task 1 is slow and task 2 finishes first, you still wait for task 1 before seeing task 2’s result. Example:
    with ThreadPoolExecutor(max_workers=4) as executor:
        results = list(executor.map(worker, inputs))

    This gives you a list where results[i] corresponds to inputs[i].

  5. Method 2: Use as_completed() with a dictionary. If you need to process results as they arrive but still want to know the original order, submit each task and store the Future object in a dictionary keyed by the input’s index. Then iterate with as_completed() and store the result in a pre‑allocated list.
    with ThreadPoolExecutor(max_workers=4) as executor:
        future_to_index = {executor.submit(worker, item): i for i, item in enumerate(inputs)}
        results = [None] * len(inputs)
        for future in as_completed(future_to_index):
            index = future_to_index[future]
            results[index] = future.result()

    Now results is in submission order.

  6. Method 3: Pass the index as an argument. Modify your worker to accept both the item and its index, and have it return a tuple (index, result). Then collect the tuples and sort by index. This is useful when you can’t use map() because you need to process results as they come, but you still want to sort later.
    def worker_with_index(item, index):
        # do work
        return (index, result)

    Then submit with a lambda or functools.partial.

  7. Handle exceptions carefully. If a task raises an exception, future.result() will re‑raise it when you call it. In the as_completed() method, wrap the call in a try/except to avoid one bad task crashing your whole loop. You can store the exception object in the results list instead of the value.
  8. Always use the context manager. The with block ensures that the executor is shut down properly, waiting for all threads to finish. If you don’t use it, call executor.shutdown(wait=True) explicitly.

Common Mistakes

  • Assuming as_completed() returns results in submission order. It returns futures as they complete, which is almost never the submission order. If you use it directly without mapping, you’ll get a jumbled order.
  • Using a shared list without locks. If you try to append results to a list from multiple threads without a lock, you can get race conditions. The methods above avoid this by using the Future objects or map(), which are thread‑safe.
  • Calling future.result() inside the worker. That’s not possible—the worker runs on the thread and doesn’t have access to the future. Always call result() from the main thread.
  • Forgetting that map() blocks. If you need to process results as they arrive (e.g., to update a progress bar), map() is not ideal because it waits for the first result even if later ones finish earlier.

Tips

  • Use map() when the order is the only thing you care about. It’s concise and less error‑prone.
  • Use as_completed() with a dictionary when you want to display results as they finish. This gives you the best of both worlds: you can show progress and still have the final list in order.
  • If you need to preserve order and also cancel tasks on early failure, consider using as_completed() and cancelling remaining futures if a critical one fails.
  • For CPU‑bound tasks, remember that threads won’t give you speedup due to the GIL. Consider ProcessPoolExecutor instead, but note that the same ordering techniques apply.
  • Test with a small number of tasks first. Use a worker that sleeps for a random time to verify your ordering logic works.

Troubleshooting

Results are out of order. If you used as_completed() without mapping, that’s the expected behavior. Re‑read Step 5 and implement the dictionary approach. If you used map() and still see disorder, check that you’re not accidentally using a set or a dictionary for inputs—those don’t guarantee order.

Program hangs or never finishes. This usually means a task is blocking indefinitely. Check your worker function for infinite loops or waiting on a lock. Also ensure you’re using the context manager so that the executor shuts down properly. If you’re using as_completed() and one task hangs, the loop will wait forever—consider adding a timeout to future.result(timeout=...).

Exceptions are swallowed. If your worker raises an exception, you won’t see it until you call future.result(). Wrap that call in a try/except and log the error. If you’re using map(), the exception will be raised when the iterator yields the corresponding result, so you might not see it until later—wrap the iteration in a try/except.

Memory usage is high. If you have a huge number of tasks, storing all futures in a dictionary can consume memory. In that case, consider batching your tasks: submit a batch of 100, collect results in order, then submit the next batch.

Frequently Asked Questions

Q: Does executor.map() guarantee order?
A: Yes, it returns results in the order of the input iterable. It blocks to maintain that order, so if the first task is slow, you wait for it even if later tasks finish earlier.

Q: Can I use as_completed() and still get ordered results?
A: Yes, by mapping each future to its index (as shown in Step 5). Store the results in a pre‑allocated list and fill in the correct position as each future completes.

Q: What is the difference between submit() and map()?
A: submit() returns a Future immediately, allowing you to track individual tasks. map() submits all tasks and returns an iterator of results in order. map() is simpler but less flexible.

Q: Why are my results not in order when using as_completed()?
A: Because as_completed() yields futures as they finish, not in submission order. You must explicitly map each future to its original index to reconstruct the order.

Q: Is it safe to modify a list from multiple threads?
A: No, not without a lock. The methods in this guide avoid that by collecting results in the main thread via Future.result() or map(), which are thread‑safe.

Q: Can I use this with ProcessPoolExecutor?
A: Yes, the same techniques apply. Just replace ThreadPoolExecutor with ProcessPoolExecutor; the Future API is identical.

Related

General steps for current mainstream versions; menus can differ by device and version. See the disclaimer or report an error.

More in this bed

The whole Tech How-To bed →