Thread Methods (sleep, join), Synchronization (Lock) & Garbage Collection
When multiple threads run simultaneously, we need ways to control their timing (sleep() and join()), prevent data corruption (Thread Synchronization using Lock), and understand how Python automatically frees unused memory (Garbage Collection).
1. Thread Control Methods: sleep() and join()
A. The sleep(seconds) Function (time Module)
- Suspends (pauses) the execution of the currently running thread for a specified number of seconds.
- While one thread is sleeping, the CPU switches to execute other active threads.
B. The join() Method (Thread Class)
- When a thread calls
t1.join(), the calling thread (usually the Main Thread) pauses and waits until threadt1finishes its execution completely before moving to the next line of code.
import threading
import time
def download_file():
print("Download started...")
time.sleep(2) # Pauses this thread for 2 seconds
print("Download completed!")
t1 = threading.Thread(target=download_file)
t1.start()
# Main thread waits here until t1 finishes!
t1.join()
print("Now opening the downloaded file in Main Thread.")
2. Thread Synchronization & Race Condition
What is a Race Condition?
When two or more threads try to modify the same shared resource (like a bank account balance or an available train seat) at the exact same time, the final data becomes corrupted or inconsistent. This bug is called a Race Condition.
What is Thread Synchronization?
Thread Synchronization is the mechanism that ensures only ONE thread can access a shared critical section at a time, preventing Race Conditions.
Synchronization Using the Lock Class (acquire() & release())
Python’s threading module provides the Lock class. A Lock acts like a key to a room:
lock.acquire()(Door Locked 🔒)
Only 1 Thread Updates Shared Data
lock.release()(Door Unlocked 🔓)
lock = threading.Lock(): Creates a new Lock object (initially in the Unlocked state).lock.acquire(): A thread locks the resource before entering the critical section. If another thread tries to calllock.acquire()at the same time, it is blocked (forced to wait) until the lock is released.lock.release(): Unlocks the resource after the critical task is done, allowing the next waiting thread to enter.
Example — Ticket Booking with Lock (acquire() & release()):
from threading import Thread, Lock
import time
class TicketCounter:
def __init__(self, available_seats):
self.available_seats = available_seats
self.lock = Lock() # Create a Lock object
def book_seat(self, name, seats_requested):
self.lock.acquire() # 🔒 Lock acquired: Only 1 thread can enter at a time
print(f"Checking seats for {name}...")
if self.available_seats >= seats_requested:
time.sleep(1)
self.available_seats -= seats_requested
print(f"SUCCESS: {seats_requested} seat(s) booked for {name}!")
else:
print(f"SORRY {name}: Only {self.available_seats} seat(s) left.")
self.lock.release() # 🔓 Lock released for next waiting thread
counter = TicketCounter(1) # Only 1 seat available!
# Two users try to book the last 1 seat at the exact same time
t1 = Thread(target=counter.book_seat, args=("Rahul", 1))
t2 = Thread(target=counter.book_seat, args=("Priya", 1))
t1.start()
t2.start()
Output (Safe — No Double Booking!):
Checking seats for Rahul...
SUCCESS: 1 seat(s) booked for Rahul!
Checking seats for Priya...
SORRY Priya: Only 0 seat(s) left.
3. Garbage Collection in Python
In languages like C, programmers must manually allocate and free memory (malloc() and free()). Forgetting to free memory causes Memory Leaks.
Garbage Collection (GC) in Python is an automatic memory management mechanism that finds and deletes objects in RAM that are no longer in use (unreachable), freeing up memory automatically.
How Python’s Garbage Collector Works:
Python uses two techniques together for automatic Garbage Collection:
- Reference Counting (Primary Mechanism):
- Every object in Python keeps a count of how many variables point to it.
- Whenever a variable is deleted (
del x) or reassigned (x = None), the object’s reference count decreases by1. - As soon as an object’s Reference Count reaches
0, Python immediately destroys the object and reclaims its memory!
- Generational Garbage Collector (Cycle Detector):
- Handles Cyclic References (when two objects reference each other in a loop, so their count never hits
0). - Python divides objects into 3 Generations (
0, 1, 2) and periodically scans them to clean up unused cycles.
- Handles Cyclic References (when two objects reference each other in a loop, so their count never hits
Controlling Garbage Collection (gc Module) & Destructor (__del__):
__del__(self)(Destructor): A special method called automatically right before Python’s garbage collector destroys an object.gcModule: Allows interacting with the Garbage Collector manually:gc.enable()— Enables automatic garbage collection.gc.disable()— Disables automatic garbage collection.gc.collect()— Forces the Garbage Collector to run immediately and free unused memory.
import gc
class DemoObject:
def __init__(self):
print("Object Created in RAM")
# Destructor called automatically when object is garbage collected
def __del__(self):
print("Object Destroyed -> Memory Freed by Garbage Collector!")
obj = DemoObject()
obj = None # Reference count becomes 0 -> Destructor called immediately!
gc.collect() # Manually trigger garbage collection cleanup