๐Ÿงต Python Tutorial โ€” Threading

Introduction ๐ŸŒŸ

Threading allows a Python program to run multiple tasks concurrently within the same process. It is extremely useful for I/O-bound tasks such as network requests, file operations, timers, and user interaction.

Note

๐Ÿ’ก Python threads run concurrently but share the same memory
๐Ÿ’ก Best for I/O-bound tasks โ€” NOT CPU-bound tasks (because of the GIL)
๐Ÿ’ก Managed using the threading module

1. Importing the Threading Module ๐Ÿงฑ

import_threading.py

import threading

2. Creating and Starting a Thread โ–ถ๏ธ

basic_thread.py

import threading
import time

def greet():
    print("Hello from thread!")
    time.sleep(1)

t = threading.Thread(target=greet)
t.start()
t.join()  # wait for thread to finish

โœ” target โ†’ function to run
โœ” start() โ†’ launches thread
โœ” join() โ†’ wait for it to finish

3. Passing Arguments to Threads ๐ŸŽฏ

thread_args.py

def show(name):
    print(f"Hello {name}")

t = threading.Thread(target=show, args=("Sathish",))
t.start()
t.join()

โœ” Use args or kwargs

4. Running Multiple Threads Concurrently โšก

multiple_threads.py

def worker(num):
    print(f"Working on task {num}")

threads = []

for i in range(5):
    t = threading.Thread(target=worker, args=(i,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

โœ” All threads run concurrently

5. Using Threading Class (Subclassing) ๐Ÿ—๏ธ

thread_class.py

class MyThread(threading.Thread):
    def run(self):
        print("Thread running...")

t = MyThread()
t.start()
t.join()

โœ” Override run() for custom behavior

6. Daemon Threads ๐Ÿ‘ป

A **daemon thread** runs in the background and exits when the main program ends.

daemon_thread.py

import time, threading

def task():
    while True:
        print("Background task running...")
        time.sleep(1)

t = threading.Thread(target=task, daemon=True)
t.start()

time.sleep(3)
print("Main program ending...")

โœ” Daemon threads stop automatically when the program stops

7. Thread Synchronization with Lock ๐Ÿ”’

Threads share memory โ†’ risk of race conditions. Use Lock to prevent unsafe simultaneous access.

lock_example.py

lock = threading.Lock()
counter = 0

def increment():
    global counter
    with lock:
        temp = counter
        temp += 1
        counter = temp

threads = []
for _ in range(1000):
    t = threading.Thread(target=increment)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print(counter)

โœ” with lock ensures thread-safe operations

8. Using RLock (Reentrant Lock) ๐Ÿ”

Allows the same thread to acquire the lock multiple times.

rlock_example.py

rlock = threading.RLock()

def task():
    with rlock:
        with rlock:  # same thread re-enters
            print("Inside RLock")

t = threading.Thread(target=task)
t.start()
t.join()

9. Using Condition for Thread Coordination ๐Ÿ•น๏ธ

condition_example.py

condition = threading.Condition()
data_ready = False

def producer():
    global data_ready
    with condition:
        data_ready = True
        print("Producer: Data ready")
        condition.notify()

def consumer():
    with condition:
        condition.wait()
        print("Consumer: Received data")

threading.Thread(target=consumer).start()
threading.Thread(target=producer).start()

โœ” Great for producerโ€“consumer workflow

10. Using Event for Signaling ๐Ÿšฆ

event_example.py

event = threading.Event()

def waiter():
    print("Waiting for event...")
    event.wait()
    print("Event triggered!")

threading.Thread(target=waiter).start()

input("Press Enter to trigger event...")
event.set()

โœ” Event helps coordinate threads

11. Thread Pool with concurrent.futures ๐Ÿ’ผ

thread_pool.py

from concurrent.futures import ThreadPoolExecutor

def task(n):
    return n * 2

with ThreadPoolExecutor(max_workers=4) as executor:
    results = executor.map(task, [1,2,3,4])
    print(list(results))

โœ” Simplest way to manage many threads

12. GIL (Global Interpreter Lock) โ€” Important โš ๏ธ

Python threads cannot run **CPU-bound tasks** in parallel due to the GIL. Use threading only for:

  • โœ” I/O-bound tasks (network, disk, waiting)
  • โœ” Background tasks

Note

โŒ Use multiprocessing for CPU-intensive tasks

13. Real-World Example โ€” Downloading Files Concurrently ๐ŸŒ

download_example.py

import threading, time

def download(file):
    print(f"Downloading {file}")
    time.sleep(2)
    print(f"Finished {file}")

files = ["a.jpg", "b.jpg", "c.jpg"]

threads = [threading.Thread(target=download, args=(f,)) for f in files]

for t in threads: t.start()
for t in threads: t.join()

Threading Cheat Sheet ๐Ÿ“˜

FeatureMethod / Object
Create threadThread(target=...)
Start threadstart()
Wait for threadjoin()
Daemon threaddaemon=True
SynchronizationLock(), RLock()
CoordinationEvent(), Condition()
Thread PoolThreadPoolExecutor

Best Practices ๐Ÿ’ก

  • โœ” Use threading only for I/O-bound tasks
  • โœ” Always use locks for shared resources
  • โœ” Prefer ThreadPoolExecutor for many tasks
  • โœ” Avoid long-running daemon threads
  • โœ” Use logging instead of print() in threaded apps

Conclusion ๐ŸŽ‰

>>โ€œThreading lets your Python programs multitask efficiently โ€” when used with the right strategy.โ€ โœจ

You now understand Threading in Python! Want the next topic? Try Multiprocessing, AsyncIO, Concurrency Models, or GIL Explained. Just tell me! ๐Ÿ˜Š