From fd78345bd45d27459916e84ab4fec628544cbcbd Mon Sep 17 00:00:00 2001 From: Syeda Abiha Ali Date: Wed, 8 Apr 2026 02:10:03 +0500 Subject: [PATCH 1/4] Add descriptive comments to chapter 02 and README.md of chapter 02 --- Chapter02/Barrier.py | 6 +- Chapter02/Condition.py | 5 + Chapter02/Event.py | 3 + Chapter02/MyThreadClass.py | 5 + Chapter02/MyThreadClass_lock.py | 9 +- Chapter02/MyThreadClass_lock_2.py | 10 +- Chapter02/README.md | 131 +++++++++++++++++++++++++ Chapter02/Rlock.py | 4 + Chapter02/Semaphore.py | 4 +- Chapter02/Thread_definition.py | 2 + Chapter02/Thread_determine.py | 3 + Chapter02/Thread_name_and_processes.py | 7 +- Chapter02/Threading_with_queue.py | 10 +- 13 files changed, 185 insertions(+), 14 deletions(-) create mode 100644 Chapter02/README.md diff --git a/Chapter02/Barrier.py b/Chapter02/Barrier.py index 7afa6f7..95f91d0 100644 --- a/Chapter02/Barrier.py +++ b/Chapter02/Barrier.py @@ -2,14 +2,18 @@ from threading import Barrier, Thread from time import ctime, sleep +# Synchronization point for a fixed number of threads num_runners = 3 finish_line = Barrier(num_runners) runners = ['Huey', 'Dewey', 'Louie'] def runner(): + # Simulate runners reaching a checkpoint at different times name = runners.pop() sleep(randrange(2, 5)) print('%s reached the barrier at: %s \n' % (name, ctime())) + + # All threads wait here until the 3rd one calls wait() finish_line.wait() def main(): @@ -23,4 +27,4 @@ def main(): print('Race over!') if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/Chapter02/Condition.py b/Chapter02/Condition.py index 828ceec..4fb671e 100644 --- a/Chapter02/Condition.py +++ b/Chapter02/Condition.py @@ -6,6 +6,7 @@ logging.basicConfig(level=logging.INFO, format=LOG_FORMAT) items = [] +# Condition object allows threads to wait for specific state changes condition = threading.Condition() @@ -16,9 +17,11 @@ def __init__(self, *args, **kwargs): def consume(self): with condition: + # Wait if there is nothing to take if len(items) == 0: logging.info('no items to consume') + # Signal the producer that there is space available condition.wait() items.pop() @@ -39,9 +42,11 @@ def __init__(self, *args, **kwargs): def produce(self): with condition: + # Wait if the buffer is full if len(items) == 10: logging.info('items produced {}. Stopped'.format(len(items))) + # Signal the consumer that a new item is ready condition.wait() items.append(1) diff --git a/Chapter02/Event.py b/Chapter02/Event.py index 4cc0eca..9787583 100644 --- a/Chapter02/Event.py +++ b/Chapter02/Event.py @@ -7,6 +7,7 @@ logging.basicConfig(level=logging.INFO, format=LOG_FORMAT) items = [] +# Event object for simple "stop/go" signaling between threads event = threading.Event() @@ -17,6 +18,7 @@ def __init__(self, *args, **kwargs): def run(self): while True: time.sleep(2) + # Blocks execution until event.set() is called by the producer event.wait() item = items.pop() logging.info('Consumer notify: {} popped by {}'\ @@ -33,6 +35,7 @@ def run(self): items.append(item) logging.info('Producer notify: item {} appended by {}'\ .format(item, self.name)) + # Trigger the event and then immediately reset it event.set() event.clear() diff --git a/Chapter02/MyThreadClass.py b/Chapter02/MyThreadClass.py index 45e6fff..74e64f4 100644 --- a/Chapter02/MyThreadClass.py +++ b/Chapter02/MyThreadClass.py @@ -3,12 +3,14 @@ from random import randint from threading import Thread +# Object-oriented approach to thread creation via subclassing class MyThreadClass (Thread): def __init__(self, name, duration): Thread.__init__(self) self.name = name self.duration = duration def run(self): + # Demonstrates that threads share the same Process ID (PID) print ("---> " + self.name + \ " running, belonging to process ID "\ + str(os.getpid()) + "\n") @@ -20,6 +22,7 @@ def main(): start_time = time.time() # Thread Creation + # Individual thread instantiation thread1 = MyThreadClass("Thread#1 ", randint(1,10)) thread2 = MyThreadClass("Thread#2 ", randint(1,10)) thread3 = MyThreadClass("Thread#3 ", randint(1,10)) @@ -31,6 +34,7 @@ def main(): thread9 = MyThreadClass("Thread#9 ", randint(1,10)) # Thread Running + # Triggering the run() method in separate threads thread1.start() thread2.start() thread3.start() @@ -42,6 +46,7 @@ def main(): thread9.start() # Thread joining + # Main thread waits for all sub-threads to finish thread1.join() thread2.join() thread3.join() diff --git a/Chapter02/MyThreadClass_lock.py b/Chapter02/MyThreadClass_lock.py index ad2f64c..b670a5d 100644 --- a/Chapter02/MyThreadClass_lock.py +++ b/Chapter02/MyThreadClass_lock.py @@ -5,6 +5,7 @@ from random import randint # Lock Definition +# Global lock to ensure mutual exclusion threadLock = threading.Lock() class MyThreadClass (Thread): @@ -13,20 +14,21 @@ def __init__(self, name, duration): self.name = name self.duration = duration def run(self): - #Acquire the Lock + # Acquire lock: only one thread can proceed past this line at a time threadLock.acquire() print ("---> " + self.name + \ " running, belonging to process ID "\ + str(os.getpid()) + "\n") time.sleep(self.duration) print ("---> " + self.name + " over\n") - #Release the Lock + # Release lock: allows the next waiting thread to proceed threadLock.release() def main(): start_time = time.time() # Thread Creation + # Initializing multiple threads thread1 = MyThreadClass("Thread#1 ", randint(1,10)) thread2 = MyThreadClass("Thread#2 ", randint(1,10)) thread3 = MyThreadClass("Thread#3 ", randint(1,10)) @@ -38,6 +40,7 @@ def main(): thread9 = MyThreadClass("Thread#9 ", randint(1,10)) # Thread Running + # Starting threads (they will wait for the lock inside the run method) thread1.start() thread2.start() thread3.start() @@ -62,7 +65,7 @@ def main(): # End print("End") - #Execution Time + #Execution Time (Total time will be high because the lock forces threads to run one by one) print("--- %s seconds ---" % (time.time() - start_time)) diff --git a/Chapter02/MyThreadClass_lock_2.py b/Chapter02/MyThreadClass_lock_2.py index 2b44f98..dad2ae3 100644 --- a/Chapter02/MyThreadClass_lock_2.py +++ b/Chapter02/MyThreadClass_lock_2.py @@ -4,7 +4,7 @@ from threading import Thread from random import randint -# Lock Definition +# Mutex used to synchronize specific sections of code threadLock = threading.Lock() class MyThreadClass (Thread): @@ -13,16 +13,16 @@ def __init__(self, name, duration): self.name = name self.duration = duration def run(self): - #Acquire the Lock + # Acquire the lock only for the print statement (Critical Section) threadLock.acquire() print ("---> " + self.name + \ " running, belonging to process ID "\ + str(os.getpid()) + "\n") + # Release immediately so others can print while this thread sleeps threadLock.release() time.sleep(self.duration) print ("---> " + self.name + " over\n") - #Release the Lock - + def main(): start_time = time.time() @@ -63,7 +63,7 @@ def main(): # End print("End") - #Execution Time + #Execution Time (Performance is better here because the time.sleep happens outside the lock) print("--- %s seconds ---" % (time.time() - start_time)) diff --git a/Chapter02/README.md b/Chapter02/README.md new file mode 100644 index 0000000..7f0ff0e --- /dev/null +++ b/Chapter02/README.md @@ -0,0 +1,131 @@ +# Chapter 02: Thread Synchronization and Coordination + +## 1. `Thread_definition.py` — Basic Thread Creation +* **Concept:** Creating and running basic threads using the `threading` module. +* **Execution:** Used a loop to create 10 threads calling `my_func`. Started and joined each one sequentially. +* **End Use:** Running a single function multiple times concurrently (e.g., background tasks). +* **When to Use:** For simple concurrent execution without needing custom classes. +* **How to Use:** Use `threading.Thread(target=..., args=...)`, then `start()` and `join()`. +* **Advantages:** Simple, clean, and requires no class overhead. +* **Disadvantages:** Sequential `start/join` in a loop prevents real concurrency; lacks complex management. + +--- + +## 2. `Thread_determine.py` — Named Threads +* **Concept:** Assigning unique names to threads for identification. +* **Execution:** Defined three functions (A, B, C) and used `threading.currentThread().getName()` to log their execution. +* **End Use:** Critical for debugging and logging in multi-threaded environments. +* **When to Use:** When you need to track specific thread behavior in complex logs. +* **How to Use:** Pass `name='...'` during thread creation; retrieve with `getName()`. +* **Advantages:** Improves debuggability; clearly identifies execution flow. +* **Disadvantages:** Only identifies threads; does not influence execution logic or priority. + +--- + +## 3. `Thread_name_and_processes.py` — Thread Class with Process ID +* **Concept:** Subclassing `Thread` and monitoring Process IDs (PIDs). +* **Execution:** Created `MyThreadClass` to print its name and PID, demonstrating that all threads share one process. +* **End Use:** Understanding the shared memory model and the limitations of the Python GIL. +* **When to Use:** When custom thread behavior is needed through object-oriented subclassing. +* **How to Use:** Inherit from `threading.Thread` and override the `run()` method. +* **Advantages:** Clean, reusable design; easy to add custom properties to threads. +* **Disadvantages:** Shared PID confirms no true CPU parallelism in standard Python (CPython). + +--- + +## 4. `MyThreadClass.py` — Custom Thread Class with Duration +* **Concept:** Simulating workloads using custom classes and random sleep intervals. +* **Execution:** Created 9 threads with varying sleep durations (1-10s) and measured total execution time. +* **End Use:** Simulating real-world concurrent tasks like file downloads or API requests. +* **When to Use:** When threads need individual properties (like specific timers or data sets). +* **How to Use:** Pass custom arguments to `__init__` and call `Thread.__init__(self)`. +* **Advantages:** Object-oriented; total time is determined by the slowest thread, not the sum. +* **Disadvantages:** Unpredictable output order; potential for "messy" console printing without locks. + +--- + +## 5. `MyThreadClass_lock.py` — Thread Lock (Sequential) +* **Concept:** Mutual Exclusion (Mutex) using `threading.Lock()`. +* **Execution:** Added a lock to the 9-thread simulation; each thread acquired the lock for its entire duration. +* **End Use:** Protecting shared resources (files/databases) from simultaneous access. +* **When to Use:** When data integrity is more important than speed. +* **How to Use:** Call `lock.acquire()` before the task and `lock.release()` after. +* **Advantages:** Prevents race conditions and ensures data integrity. +* **Disadvantages:** Eliminates concurrency benefits; risk of deadlocks if a lock isn't released. + +--- + +## 6. `MyThreadClass_lock_2.py` — Thread Lock (Optimized) +* **Concept:** Fine-grained locking to improve performance. +* **Execution:** Released the lock before `time.sleep()`, allowing other threads to enter their critical sections during the wait. +* **End Use:** Balancing data protection with execution speed. +* **When to Use:** When only a small portion of the thread's work (like a print or write) needs protection. +* **How to Use:** Keep the "locked" section as short as possible. +* **Advantages:** Much faster than full-task locking; allows threads to overlap during non-critical work. +* **Disadvantages:** Requires careful analysis of what truly needs to be "locked." + +--- + +## 7. `Rlock.py` — Reentrant Lock +* **Concept:** Using `threading.RLock()` to allow nested lock acquisition. +* **Execution:** Implemented a `Box` class where methods calling each other both required the same lock. +* **End Use:** Solving deadlocks in recursive functions or nested method calls. +* **When to Use:** When a thread needs to re-acquire a lock it already holds. +* **How to Use:** Replace `Lock()` with `RLock()`; must be released as many times as it is acquired. +* **Advantages:** Prevents "self-deadlock" in complex class structures. +* **Disadvantages:** Slightly more overhead than a standard lock. + +--- + +## 8. `Semaphore.py` — Semaphore for Signaling +* **Concept:** Controlling resource access via `threading.Semaphore()`. +* **Execution:** Used a semaphore (starting at 0) to force a consumer to wait for a producer's signal. +* **End Use:** Managing resource pools or simple producer-consumer signaling. +* **When to Use:** To limit the number of threads accessing a resource or for basic synchronization. +* **How to Use:** `acquire()` decrements the counter; `release()` increments it and wakes waiting threads. +* **Advantages:** Precise control over the number of allowed concurrent threads. +* **Disadvantages:** Can lead to race conditions on shared variables if not used with a lock. + +--- + +## 9. `Event.py` — Thread Event Signaling +* **Concept:** One-to-many signaling using `threading.Event()`. +* **Execution:** A Producer sets an event after creating data; a Consumer waits for the event to trigger. +* **End Use:** Notifying threads that a specific condition (like "Data Ready") has been met. +* **When to Use:** For simple "stop/go" signals between threads. +* **How to Use:** Use `event.wait()` to block and `event.set()` to signal all waiting threads. +* **Advantages:** Simple and clean; does not require manual counter management. +* **Disadvantages:** Risk of missing signals if `clear()` is called too quickly. + +--- + +## 10. `Condition.py` — Thread Condition Variable +* **Concept:** Complex coordination using `threading.Condition()`. +* **Execution:** Managed a buffer where the Producer waits if the list is full and the Consumer waits if it's empty. +* **End Use:** Sophisticated Producer-Consumer models with state-dependent logic. +* **When to Use:** When threads must wait for a specific state change in shared data. +* **How to Use:** Use `with condition:`, `wait()` to pause, and `notify()` to wake others. +* **Advantages:** Prevents both buffer overflow and underflow; more powerful than Events. +* **Disadvantages:** Higher complexity; prone to bugs if state logic is incorrect. + +--- + +## 11. `Barrier.py` — Thread Barrier Synchronization +* **Concept:** Synchronizing multiple threads at a specific checkpoint. +* **Execution:** Three "runner" threads wait at a `finish_line.wait()` until all arrive before proceeding. +* **End Use:** Phased parallel algorithms where all parts must finish before the next step starts. +* **When to Use:** When you need a "meeting" point for a fixed number of threads. +* **How to Use:** Define `Barrier(n)`; all threads call `wait()`. +* **Advantages:** Guarantees all threads stay "in sync" through different execution phases. +* **Disadvantages:** If one thread fails to reach the barrier, all other threads block indefinitely. + +--- + +## 12. `Threading_with_queue.py` — Thread-Safe Queue +* **Concept:** Using the `queue.Queue` class for safe data exchange. +* **Execution:** One producer adds items to a queue while three consumers process them concurrently. +* **End Use:** Standard practice for producer-consumer patterns in Python. +* **When to Use:** Whenever threads need to share data safely without manual locking logic. +* **How to Use:** Use `put()` to add data and `get()` to retrieve it. +* **Advantages:** Automatically handles all internal locking; highly scalable with multiple consumers. +* **Disadvantages:** Requires a "poison pill" or timeout to stop consumer threads gracefully. \ No newline at end of file diff --git a/Chapter02/Rlock.py b/Chapter02/Rlock.py index 25bc64b..1188622 100644 --- a/Chapter02/Rlock.py +++ b/Chapter02/Rlock.py @@ -5,19 +5,23 @@ class Box: def __init__(self): + # RLock allows the same thread to acquire the lock multiple times self.lock = threading.RLock() self.total_items = 0 def execute(self, value): + # Nested locking: called by add/remove while they already hold the lock with self.lock: self.total_items += value def add(self): + # Calls another method that also requests the same lock with self.lock: self.execute(1) def remove(self): with self.lock: + # Calls another method that also requests the same lock self.execute(-1) def adder(box, items): diff --git a/Chapter02/Semaphore.py b/Chapter02/Semaphore.py index 446088d..fb24978 100644 --- a/Chapter02/Semaphore.py +++ b/Chapter02/Semaphore.py @@ -6,13 +6,14 @@ LOG_FORMAT = '%(asctime)s %(threadName)-17s %(levelname)-8s %(message)s' logging.basicConfig(level=logging.INFO, format=LOG_FORMAT) - +# Semaphore starting at 0 forces the consumer to wait for producer's release semaphore = threading.Semaphore(0) item = 0 def consumer(): logging.info('Consumer is waiting') + # Decrements semaphore; blocks here because initial value is 0 semaphore.acquire() logging.info('Consumer notify: item number {}'.format(item)) @@ -22,6 +23,7 @@ def producer(): time.sleep(3) item = random.randint(0, 1000) logging.info('Producer notify: item number {}'.format(item)) + # Decrements semaphore; blocks here because initial value is 0 semaphore.release() diff --git a/Chapter02/Thread_definition.py b/Chapter02/Thread_definition.py index 3797983..5bfc1ec 100644 --- a/Chapter02/Thread_definition.py +++ b/Chapter02/Thread_definition.py @@ -8,9 +8,11 @@ def my_func(thread_number): def main(): threads = [] for i in range(10): + # Initializing thread with arguments t = threading.Thread(target=my_func, args=(i,)) threads.append(t) t.start() + # Immediately joining ensures threads run sequentially in this specific case t.join() if __name__ == "__main__": diff --git a/Chapter02/Thread_determine.py b/Chapter02/Thread_determine.py index 58f98b5..6e44e61 100644 --- a/Chapter02/Thread_determine.py +++ b/Chapter02/Thread_determine.py @@ -1,7 +1,9 @@ import threading import time +# These functions demonstrate how to identify which thread is executing a specific task def function_A(): + # .getName() identifies the thread by its assigned string name print (threading.currentThread().getName()+str('--> starting \n')) time.sleep(2) print (threading.currentThread().getName()+str( '--> exiting \n')) @@ -22,6 +24,7 @@ def function_C(): if __name__ == "__main__": + # Assigning explicit names during thread creation for easier logging and debugging t1 = threading.Thread(name='function_A', target=function_A) t2 = threading.Thread(name='function_B', target=function_B) t3 = threading.Thread(name='function_C',target=function_C) diff --git a/Chapter02/Thread_name_and_processes.py b/Chapter02/Thread_name_and_processes.py index ae78e30..c6c9599 100644 --- a/Chapter02/Thread_name_and_processes.py +++ b/Chapter02/Thread_name_and_processes.py @@ -2,26 +2,29 @@ import time import os +# Subclassing the Thread class for a custom implementation class MyThreadClass (Thread): def __init__(self, name): Thread.__init__(self) self.name = name def run(self): + # Subclassing the Thread class for a custom implementation print("ID of process running {}".format(self.name)) #, " is {} \n".format(os.getpid())) def main(): from random import randint # Thread Creation + # Creating instances of the custom thread class thread1 = MyThreadClass("Thread#1 ") thread2 = MyThreadClass("Thread#2 ") - # Thread Running + # Thread Running (Starting the threads triggers the run() method) thread1.start() thread2.start() - # Thread joining + # Thread joining (Ensuring the main program waits for these threads to complete) thread1.join() thread2.join() diff --git a/Chapter02/Threading_with_queue.py b/Chapter02/Threading_with_queue.py index 5cebee6..6ed8f6c 100644 --- a/Chapter02/Threading_with_queue.py +++ b/Chapter02/Threading_with_queue.py @@ -5,7 +5,7 @@ import time import random - +# The Producer adds data to a thread-safe Queue class Producer(Thread): def __init__(self, queue): @@ -15,12 +15,13 @@ def __init__(self, queue): def run(self): for i in range(5): item = random.randint(0, 256) + # .put() handles all internal locking automatically self.queue.put(item) print('Producer notify : item N°%d appended to queue by %s\n'\ % (item, self.name)) time.sleep(1) - +# Consumers retrieve data from the same Queue concurrently class Consumer(Thread): def __init__(self, queue): @@ -29,12 +30,15 @@ def __init__(self, queue): def run(self): while True: + # .get() will block if the queue is empty until an item is available item = self.queue.get() print('Consumer notify : %d popped from queue by %s'\ % (item, self.name)) + # Signals that the specific item has been processed self.queue.task_done() if __name__ == '__main__': + # A Queue object is inherently thread-safe for many-to-many communication queue = Queue() t1 = Producer(queue) @@ -42,11 +46,13 @@ def run(self): t3 = Consumer(queue) t4 = Consumer(queue) + # Note: Consumer threads in this script will run indefinitely t1.start() t2.start() t3.start() t4.start() + # The consumer joins would block indefinitely here as they are in 'while True' loops t1.join() t2.join() t3.join() From c0315aa462c58fbc5da3f14a67d8310cd542b8e2 Mon Sep 17 00:00:00 2001 From: Syeda Abiha Ali Date: Wed, 8 Apr 2026 03:00:07 +0500 Subject: [PATCH 2/4] Add README for Chapter 01 --- Chapter01/README.md | 135 ++++++++++++++++++++++++++++++++++++++++++++ Chapter02/README.md | 2 +- 2 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 Chapter01/README.md diff --git a/Chapter01/README.md b/Chapter01/README.md new file mode 100644 index 0000000..00ae879 --- /dev/null +++ b/Chapter01/README.md @@ -0,0 +1,135 @@ +# Chapter 02: Thread Synchronization and Coordination + +This chapter explores advanced threading concepts in Python, focusing on how to manage, identify, and synchronize multiple threads to prevent data corruption and coordinate complex tasks. + +--- + +## 1. `Thread_definition.py` — Basic Thread Creation +* **Concept:** Creating and running basic threads using the `threading` module. +* **Execution:** Used a loop to create 10 threads calling `my_func`. Started and joined each one sequentially. +* **End Use:** Running a single function multiple times concurrently (e.g., background tasks). +* **When to Use:** For simple concurrent execution without needing custom classes. +* **How to Use:** Use `threading.Thread(target=..., args=...)`, then `start()` and `join()`. +* **Advantages:** Simple, clean, and requires no class overhead. +* **Disadvantages:** Sequential `start/join` in a loop prevents real concurrency; lacks complex management. + +--- + +## 2. `Thread_determine.py` — Named Threads +* **Concept:** Assigning unique names to threads for identification. +* **Execution:** Defined three functions (A, B, C) and used `threading.currentThread().getName()` to log their execution. +* **End Use:** Critical for debugging and logging in multi-threaded environments. +* **When to Use:** When you need to track specific thread behavior in complex logs. +* **How to Use:** Pass `name='...'` during thread creation; retrieve with `getName()`. +* **Advantages:** Improves debuggability; clearly identifies execution flow. +* **Disadvantages:** Only identifies threads; does not influence execution logic or priority. + +--- + +## 3. `Thread_name_and_processes.py` — Thread Class with Process ID +* **Concept:** Subclassing `Thread` and monitoring Process IDs (PIDs). +* **Execution:** Created `MyThreadClass` to print its name and PID, demonstrating that all threads share one process. +* **End Use:** Understanding the shared memory model and the limitations of the Python GIL. +* **When to Use:** When custom thread behavior is needed through object-oriented subclassing. +* **How to Use:** Inherit from `threading.Thread` and override the `run()` method. +* **Advantages:** Clean, reusable design; easy to add custom properties to threads. +* **Disadvantages:** Shared PID confirms no true CPU parallelism in standard Python (CPython). + +--- + +## 4. `MyThreadClass.py` — Custom Thread Class with Duration +* **Concept:** Simulating workloads using custom classes and random sleep intervals. +* **Execution:** Created 9 threads with varying sleep durations (1-10s) and measured total execution time. +* **End Use:** Simulating real-world concurrent tasks like file downloads or API requests. +* **When to Use:** When threads need individual properties (like specific timers or data sets). +* **How to Use:** Pass custom arguments to `__init__` and call `Thread.__init__(self)`. +* **Advantages:** Object-oriented; total time is determined by the slowest thread, not the sum. +* **Disadvantages:** Unpredictable output order; potential for "messy" console printing without locks. + +--- + +## 5. `MyThreadClass_lock.py` — Thread Lock (Sequential) +* **Concept:** Mutual Exclusion (Mutex) using `threading.Lock()`. +* **Execution:** Added a lock to the 9-thread simulation; each thread acquired the lock for its entire duration. +* **End Use:** Protecting shared resources (files/databases) from simultaneous access. +* **When to Use:** When data integrity is more important than speed. +* **How to Use:** Call `lock.acquire()` before the task and `lock.release()` after. +* **Advantages:** Prevents race conditions and ensures data integrity. +* **Disadvantages:** Eliminates concurrency benefits; risk of deadlocks if a lock isn't released. + +--- + +## 6. `MyThreadClass_lock_2.py` — Thread Lock (Optimized) +* **Concept:** Fine-grained locking to improve performance. +* **Execution:** Released the lock before `time.sleep()`, allowing other threads to enter their critical sections during the wait. +* **End Use:** Balancing data protection with execution speed. +* **When to Use:** When only a small portion of the thread's work (like a print or write) needs protection. +* **How to Use:** Keep the "locked" section as short as possible. +* **Advantages:** Much faster than full-task locking; allows threads to overlap during non-critical work. +* **Disadvantages:** Requires careful analysis of what truly needs to be "locked." + +--- + +## 7. `Rlock.py` — Reentrant Lock +* **Concept:** Using `threading.RLock()` to allow nested lock acquisition. +* **Execution:** Implemented a `Box` class where methods calling each other both required the same lock. +* **End Use:** Solving deadlocks in recursive functions or nested method calls. +* **When to Use:** When a thread needs to re-acquire a lock it already holds. +* **How to Use:** Replace `Lock()` with `RLock()`; must be released as many times as it is acquired. +* **Advantages:** Prevents "self-deadlock" in complex class structures. +* **Disadvantages:** Slightly more overhead than a standard lock. + +--- + +## 8. `Semaphore.py` — Semaphore for Signaling +* **Concept:** Controlling resource access via `threading.Semaphore()`. +* **Execution:** Used a semaphore (starting at 0) to force a consumer to wait for a producer's signal. +* **End Use:** Managing resource pools or simple producer-consumer signaling. +* **When to Use:** To limit the number of threads accessing a resource or for basic synchronization. +* **How to Use:** `acquire()` decrements the counter; `release()` increments it and wakes waiting threads. +* **Advantages:** Precise control over the number of allowed concurrent threads. +* **Disadvantages:** Can lead to race conditions on shared variables if not used with a lock. + +--- + +## 9. `Event.py` — Thread Event Signaling +* **Concept:** One-to-many signaling using `threading.Event()`. +* **Execution:** A Producer sets an event after creating data; a Consumer waits for the event to trigger. +* **End Use:** Notifying threads that a specific condition (like "Data Ready") has been met. +* **When to Use:** For simple "stop/go" signals between threads. +* **How to Use:** Use `event.wait()` to block and `event.set()` to signal all waiting threads. +* **Advantages:** Simple and clean; does not require manual counter management. +* **Disadvantages:** Risk of missing signals if `clear()` is called too quickly. + +--- + +## 10. `Condition.py` — Thread Condition Variable +* **Concept:** Complex coordination using `threading.Condition()`. +* **Execution:** Managed a buffer where the Producer waits if the list is full and the Consumer waits if it's empty. +* **End Use:** Sophisticated Producer-Consumer models with state-dependent logic. +* **When to Use:** When threads must wait for a specific state change in shared data. +* **How to Use:** Use `with condition:`, `wait()` to pause, and `notify()` to wake others. +* **Advantages:** Prevents both buffer overflow and underflow; more powerful than Events. +* **Disadvantages:** Higher complexity; prone to bugs if state logic is incorrect. + +--- + +## 11. `Barrier.py` — Thread Barrier Synchronization +* **Concept:** Synchronizing multiple threads at a specific checkpoint. +* **Execution:** Three "runner" threads wait at a `finish_line.wait()` until all arrive before proceeding. +* **End Use:** Phased parallel algorithms where all parts must finish before the next step starts. +* **When to Use:** When you need a "rendezvous" point for a fixed number of threads. +* **How to Use:** Define `Barrier(n)`; all threads call `wait()`. +* **Advantages:** Guarantees all threads stay "in sync" through different execution phases. +* **Disadvantages:** If one thread fails to reach the barrier, all other threads block indefinitely. + +--- + +## 12. `Threading_with_queue.py` — Thread-Safe Queue +* **Concept:** Using the `queue.Queue` class for safe data exchange. +* **Execution:** One producer adds items to a queue while three consumers process them concurrently. +* **End Use:** Standard practice for producer-consumer patterns in Python. +* **When to Use:** Whenever threads need to share data safely without manual locking logic. +* **How to Use:** Use `put()` to add data and `get()` to retrieve it. +* **Advantages:** Automatically handles all internal locking; highly scalable with multiple consumers. +* **Disadvantages:** Requires a "poison pill" or timeout to stop consumer threads gracefully. \ No newline at end of file diff --git a/Chapter02/README.md b/Chapter02/README.md index 7f0ff0e..e48caef 100644 --- a/Chapter02/README.md +++ b/Chapter02/README.md @@ -8,7 +8,7 @@ * **How to Use:** Use `threading.Thread(target=..., args=...)`, then `start()` and `join()`. * **Advantages:** Simple, clean, and requires no class overhead. * **Disadvantages:** Sequential `start/join` in a loop prevents real concurrency; lacks complex management. - +git commit --amend -m "Add descriptive comments to Chapter 01 and 02" --- ## 2. `Thread_determine.py` — Named Threads From dee9c67d186666e62386e84eb09f098412c447ec Mon Sep 17 00:00:00 2001 From: Syeda Abiha Ali Date: Wed, 8 Apr 2026 03:08:54 +0500 Subject: [PATCH 3/4] ADD Chapter 02 README --- Chapter02/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Chapter02/README.md b/Chapter02/README.md index e48caef..b593cb9 100644 --- a/Chapter02/README.md +++ b/Chapter02/README.md @@ -128,4 +128,5 @@ git commit --amend -m "Add descriptive comments to Chapter 01 and 02" * **When to Use:** Whenever threads need to share data safely without manual locking logic. * **How to Use:** Use `put()` to add data and `get()` to retrieve it. * **Advantages:** Automatically handles all internal locking; highly scalable with multiple consumers. -* **Disadvantages:** Requires a "poison pill" or timeout to stop consumer threads gracefully. \ No newline at end of file +* **Disadvantages:** Requires a "poison pill" or timeout to stop consumer threads gracefully. + From f23581d6a38b539fcd390bcb4bfd6cc846eb3fad Mon Sep 17 00:00:00 2001 From: Syeda Abiha Ali Date: Wed, 8 Apr 2026 03:39:18 +0500 Subject: [PATCH 4/4] Cleanup README formatting --- Chapter02/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/Chapter02/README.md b/Chapter02/README.md index b593cb9..048f656 100644 --- a/Chapter02/README.md +++ b/Chapter02/README.md @@ -8,7 +8,6 @@ * **How to Use:** Use `threading.Thread(target=..., args=...)`, then `start()` and `join()`. * **Advantages:** Simple, clean, and requires no class overhead. * **Disadvantages:** Sequential `start/join` in a loop prevents real concurrency; lacks complex management. -git commit --amend -m "Add descriptive comments to Chapter 01 and 02" --- ## 2. `Thread_determine.py` — Named Threads