Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions Chapter01/README.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 5 additions & 1 deletion Chapter02/Barrier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -23,4 +27,4 @@ def main():
print('Race over!')

if __name__ == "__main__":
main()
main()
5 changes: 5 additions & 0 deletions Chapter02/Condition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand All @@ -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()
Expand All @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions Chapter02/Event.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand All @@ -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 {}'\
Expand All @@ -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()

Expand Down
5 changes: 5 additions & 0 deletions Chapter02/MyThreadClass.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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))
Expand All @@ -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()
Expand All @@ -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()
Expand Down
9 changes: 6 additions & 3 deletions Chapter02/MyThreadClass_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from random import randint

# Lock Definition
# Global lock to ensure mutual exclusion
threadLock = threading.Lock()

class MyThreadClass (Thread):
Expand All @@ -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))
Expand All @@ -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()
Expand All @@ -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))


Expand Down
10 changes: 5 additions & 5 deletions Chapter02/MyThreadClass_lock_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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()
Expand Down Expand Up @@ -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))


Expand Down
Loading