Parallel downloads using Threading in Python

This is a follow up to our previous article Asynchronous downloads using asyncio in Python where we used asyncio to achieve concurrency.

We will attempt to re-write the same code using threading, specifically concurrent.futures.ThreadPoolExecutor which offers a higher level interface to push tasks to a background thread without blocking execution of the calling thread, while still being able to retrieve their results when needed.

For our retry strategy, we use tenacity – a general-purpose retrying library to simplify the task of adding retry behavior to just about anything. pip install tenacity

  • @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), retry=retry_if_exception_type(requests.RequestException)): Retries the download up to 3 times, waiting exponentially between attempts (2 seconds minimum and 10 seconds maximum), and only retries on requests.RequestException.
  • main(urls, max_workers=10) manages the concurrent downloading.
  • Uses a ThreadPoolExecutor with a maximum of max_workers threads.
  • Submits each URL to the thread pool for downloading.
  • Waits for each future to complete and collect results.

Putting it all together

Output

Note: Unlike asyncio which has a single-threaded event loop, threading uses multiple threads running in parallel.

Further Reading