Threading

Back

Loading concept...

🧵 Python Threading: Making Your Code Do Many Things at Once!

Imagine a Kitchen with Many Chefs!

One chef alone takes forever to cook a big meal. But what if you had many chefs working together? They can chop vegetables, boil water, and stir the soup—all at the same time! That’s exactly what threading does for your Python code.


🌟 The Big Picture

In Python, your code usually runs one instruction at a time—like a single chef in the kitchen. But with threading, you can have multiple workers (called threads) doing different tasks simultaneously!

graph TD A["Your Program"] --> B["Thread 1: Download File"] A --> C["Thread 2: Process Data"] A --> D["Thread 3: Update Screen"] B --> E["All Done Together!"] C --> E D --> E

1️⃣ Thread Creation

What is it?

Creating a thread is like hiring a new chef for your kitchen. You tell them what job to do, and they get to work!

The Simple Way

import threading

def say_hello():
    print("Hello from thread!")

# Create a new thread
my_thread = threading.Thread(
    target=say_hello
)
my_thread.start()

What’s happening here?

  • target=say_hello → The job we want done
  • .start() → “Go, chef! Start cooking!”

Pass Some Ingredients (Arguments)

def greet(name, times):
    for i in range(times):
        print(f"Hi, {name}!")

thread = threading.Thread(
    target=greet,
    args=("Alice", 3)
)
thread.start()

The Class Way (For Fancier Chefs)

class MyThread(threading.Thread):
    def run(self):
        print("I'm running!")

chef = MyThread()
chef.start()

💡 Key Idea: Creating a thread = hiring a worker. Starting it = telling them to begin!


2️⃣ Managing Threads

Waiting for Your Chef to Finish

Sometimes you need to wait until a thread finishes its work before moving on.

thread = threading.Thread(
    target=slow_task
)
thread.start()
thread.join()  # Wait here!
print("Thread finished!")

The .join() method is like saying: “I’ll wait right here until you’re done.”

Check if Thread is Still Working

if thread.is_alive():
    print("Still cooking!")
else:
    print("All done!")

Daemon Threads (Background Helpers)

Some threads are like cleaning staff—they work in the background and stop when the restaurant closes.

helper = threading.Thread(
    target=background_work,
    daemon=True
)
helper.start()
# When main program ends,
# daemon threads stop too!

Naming Your Threads

thread = threading.Thread(
    target=task,
    name="DataLoader"
)
print(thread.name)  # DataLoader

🎯 Remember: Use .join() to wait, .is_alive() to check, and daemon=True for background helpers!


3️⃣ The GIL Explained

What is the GIL?

GIL = Global Interpreter Lock

Imagine your kitchen has only ONE special spoon that every chef needs to use. Even with 10 chefs, only ONE can use the spoon at a time!

graph TD A["Thread 1"] -->|wants| S["🥄 GIL"] B["Thread 2"] -->|waits| S C["Thread 3"] -->|waits| S S -->|only one<br>at a time| D["Python Code"]

Why Does Python Have This?

Python’s memory management isn’t safe when multiple threads access it simultaneously. The GIL is like a traffic light—it prevents crashes!

The Good News

For tasks that wait (like downloading files or reading databases), threads still help a lot!

# This DOES benefit from threads!
# While one thread waits for
# download, another can work
thread1: downloading...waiting...
thread2: processing...
thread3: uploading...

The Not-So-Good News

For heavy math (CPU work), threads won’t speed things up much.

# This WON'T go faster with threads
# Both need the CPU constantly
thread1: calculate...calculate...
thread2: must wait for GIL...

🧠 Simple Rule:

  • Waiting tasks (I/O) → Threads help! ✅
  • Math tasks (CPU) → Use multiprocessing instead! 🔄

4️⃣ Synchronization Primitives

The Problem

When multiple chefs use the same ingredient bowl, things can go wrong!

# DANGER! Both threads change
# the same variable!
counter = 0

def add_one():
    global counter
    counter = counter + 1
    # What if another thread
    # changes counter HERE?

Solution 1: Lock (The Kitchen Pass)

Only the chef with the pass can touch the ingredient.

lock = threading.Lock()
counter = 0

def safe_add():
    global counter
    lock.acquire()    # Take pass
    counter += 1
    lock.release()    # Return pass

Even Better - Use with:

with lock:
    counter += 1
# Lock auto-releases!

Solution 2: RLock (Reentrant Lock)

A lock that the SAME chef can acquire multiple times.

rlock = threading.RLock()

def nested_function():
    with rlock:
        # Can acquire again!
        with rlock:
            print("Still works!")

Solution 3: Semaphore (Limited Passes)

Only allow N chefs at once.

# Only 3 threads can enter
sem = threading.Semaphore(3)

with sem:
    use_limited_resource()

Solution 4: Event (Signal Flag)

One thread signals others to go!

event = threading.Event()

# Thread 1: Wait for signal
event.wait()
print("Got the signal!")

# Thread 2: Send signal
event.set()  # "Go ahead!"

Solution 5: Condition (Wait for Change)

cond = threading.Condition()

# Consumer waits
with cond:
    cond.wait()  # Sleep until notified
    process_item()

# Producer notifies
with cond:
    add_item()
    cond.notify()  # "Wake up!"

🛡️ Golden Rule: When threads share data, ALWAYS use locks!


5️⃣ Thread-Safe Queues

The Perfect Solution for Sharing

A Queue is like a conveyor belt in a restaurant. Chefs put dishes on, waiters take them off—safely!

graph LR A["Chef 1"] -->|put| Q["📦 Queue"] B["Chef 2"] -->|put| Q Q -->|get| C["Waiter 1"] Q -->|get| D["Waiter 2"]

Basic Queue Usage

from queue import Queue

q = Queue()

# Add items
q.put("pizza")
q.put("pasta")

# Get items (waits if empty)
item = q.get()
print(item)  # pizza

Producer-Consumer Pattern

from queue import Queue
import threading

q = Queue()

def producer():
    for i in range(5):
        q.put(f"item_{i}")

def consumer():
    while True:
        item = q.get()
        print(f"Got: {item}")
        q.task_done()

# Start workers
threading.Thread(
    target=producer
).start()
threading.Thread(
    target=consumer,
    daemon=True
).start()

q.join()  # Wait until all done

Queue Types

Type Description
Queue() First in, first out
LifoQueue() Last in, first out (stack)
PriorityQueue() Sorted by priority

Useful Methods

q.put(item)      # Add (waits if full)
q.get()          # Remove (waits if empty)
q.empty()        # Check if empty
q.full()         # Check if full
q.qsize()        # Current size
q.task_done()    # Mark item processed
q.join()         # Wait for all done

📦 Pro Tip: When threads need to share data, use a Queue instead of a regular list!


6️⃣ ThreadPoolExecutor

What is it?

Instead of creating threads yourself, use a pool of ready workers! It’s like having a team of chefs always ready to cook.

graph TD A["Your Tasks"] --> B["Thread Pool"] B --> C["Worker 1"] B --> D["Worker 2"] B --> E["Worker 3"] C --> F["Results"] D --> F E --> F

Basic Usage

from concurrent.futures import (
    ThreadPoolExecutor
)

def process(item):
    return item * 2

with ThreadPoolExecutor(
    max_workers=4
) as pool:
    result = pool.submit(
        process, 10
    )
    print(result.result())  # 20

Process Many Items

items = [1, 2, 3, 4, 5]

with ThreadPoolExecutor(
    max_workers=3
) as pool:
    results = pool.map(
        process, items
    )
    print(list(results))
    # [2, 4, 6, 8, 10]

Getting Results

future = pool.submit(task, arg)

# Check if done
future.done()  # True/False

# Wait and get result
value = future.result()

# Set timeout
value = future.result(timeout=5)

Handle Multiple Futures

from concurrent.futures import (
    as_completed
)

futures = [
    pool.submit(task, i)
    for i in range(5)
]

for future in as_completed(futures):
    print(future.result())

Why Use ThreadPoolExecutor?

Manual Threads ThreadPoolExecutor
Create each thread Workers ready
Manage yourself Auto-managed
Easy to forget cleanup Auto cleanup
Complex error handling Easy errors

🚀 Best Practice: For most threading needs, use ThreadPoolExecutor—it’s simpler and safer!


🎓 Quick Summary

Concept One-Line Explanation
Thread Creation Thread(target=func).start()
Managing Threads .join() to wait, .is_alive() to check
GIL Only one thread runs Python at a time
Lock with lock: to protect shared data
Queue Safe way for threads to share data
ThreadPoolExecutor Pre-made team of worker threads

🌈 You Did It!

Threading is like running a busy kitchen with many chefs. Now you know how to:

  1. ✅ Hire chefs (create threads)
  2. ✅ Manage them (join, daemon)
  3. ✅ Understand the one-spoon rule (GIL)
  4. ✅ Keep them from fighting (locks)
  5. ✅ Share ingredients safely (queues)
  6. ✅ Use a ready team (ThreadPoolExecutor)

Go forth and make your code faster! 🚀

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.