Skip to Content
Course content

201: Threading Basics with the threading Module

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to talk about a moment most Python developers hit: the realization that your code is spending 90% of its time doing absolutely nothing. Specifically, I'm talking about I/O-bound tasks. If you're writing a script that calls five different APIs or downloads ten images, you've probably just written a loop that handles them one by one. It works, but it's painfully slow because your CPU is just sitting there, idling, waiting for a server halfway across the world to send a packet back.

The Cost of Waiting in Line

Let's look at the naive way to handle this. Imagine we have a list of URLs and a function that simulates a network request using time.sleep(). In a standard sequential script, it looks like this:

import time

def fetch_data(url):
    print(f"Fetching {url}...")
    time.sleep(2)  # Simulating network latency
    print(f"Done with {url}")

urls = ["api/users", "api/posts", "api/comments", "api/settings"]

start = time.time()
for url in urls:
    fetch_data(url)
print(f"Total time: {time.time() - start:.2f} seconds")

If you run this, it'll take about 8 seconds. Why? Because the program is strictly linear. It fetches the users, waits two seconds, then moves to posts, waits two seconds, and so on. You're essentially standing in a single-file line at a coffee shop where only one person can order at a time, even though there are four baristas standing around doing nothing.

Breaking the Line with Threading

This is where the threading module comes in. Threading allows us to start a task and, instead of waiting for it to finish before moving to the next line of code, we tell Python: "Start this in the background and let me know when it's done."

Here is how I'd refactor that loop to actually utilize the downtime. I'll use a list to keep track of the threads so we can ensure the main program doesn't exit before the background work is finished:

import threading
import time

def fetch_data(url):
    print(f"Fetching {url}...")
    time.sleep(2)
    print(f"Done with {url}")

urls = ["api/users", "api/posts", "api/comments", "api/settings"]
threads = []

start = time.time()
for url in urls:
    # We create a Thread object, telling it which function to run and what arguments to use
    thread = threading.Thread(target=fetch_data, args=(url,))
    threads.append(thread)
    thread.start()

# This is the crucial part: join() tells the main program to wait for the thread to finish
for thread in threads:
    thread.join()

print(f"Total time: {time.time() - start:.2f} seconds")

Now, the total time drops to about 2 seconds. We've essentially opened four different checkout lanes at that coffee shop. The CPU sends the request for the first URL, and while it's waiting for the response, it immediately jumps to start the second thread, then the third, and so on.

When Threading Hits a Wall

Now, I need to give you a fair warning: threading in Python isn't a magic "make it fast" button for everything. You've probably heard of the GIL—the Global Interpreter Lock. In simple terms, the GIL ensures that only one thread executes Python bytecode at a time.

If your task is CPU-bound—like calculating a billion digits of Pi or processing a massive image—threading won't help you. In fact, it might actually slow you down because of the overhead involved in switching between threads. Threading is for waiting. If your code is waiting on a network, a disk read, or a user input, use threading. If your code is crunching numbers, you'll need to look into the multiprocessing module, which we'll cover later.

One last thing: managing a list of threads manually, like I did above, gets messy fast. If you're dealing with dozens or hundreds of tasks, you don't want to spawn 100 individual threads; you'll exhaust your system resources. In a professional production environment, I'd suggest using concurrent.futures.ThreadPoolExecutor, which manages a "pool" of threads for you. But before you jump to the high-level abstractions, you need to understand the underlying threading.Thread logic I've shown here.




📋 Practical Task

Build a Parallel Website Status Checker

Your task is to create a script that checks the availability of multiple websites simultaneously. Instead of using a real network library like requests (to keep this focused on threading logic), you will simulate the check using time.sleep().

  • Create a list of at least 6 dummy website URLs.
  • Write a function check_status(url) that prints "Checking [url]...", sleeps for a random duration between 1 and 3 seconds (use random.uniform(1, 3)), and then prints "[url] is ONLINE".
  • Implement the threading logic to launch these checks in parallel.
  • Ensure that the main program waits for all threads to complete using .join() before printing a final message: "All websites have been checked."
  • Measure and print the total execution time to prove that it took significantly less time than if the checks were run sequentially.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.