I was a bit taken aback to come to know about condition-variables
this late in my journey. Condition variables allow you to wait
and notify
other threads when a certain event occurs. I understood it best through an example.
Following is a code sample for Producer-Consumer
problem using mutexes.
producer-consumer-just-mutex.py
from threading import Thread, Lock
import time, random
mutex = Lock()
items = []
def timer(t):
while t:
time.sleep(1)
print(f"Time: {t}")
t -= 1
def producer():
while True:
with mutex:
print("Acquired lock in producer")
items.append(["say - woooo", "I won't let u gooo"] * random.randint(1, 10))
print("Released lock in producer")
timer_thread = Thread(target=timer, args=(10,))
timer_thread.start()
timer_thread.join()
def consumer():
empty_loop_count = 0
while True:
empty_loop_count += 1
with mutex:
if items:
print(f"Got items after {empty_loop_count} empty loops")
empty_loop_count = 0
print(items)
items.clear()
producer_thread = Thread(target=producer)
consumer_thread = Thread(target=consumer)
producer_thread.start()
consumer_thread.start()
As you can see below, we are wasting a lot of CPU cycles to continuously check the value of mutex while the producer is intentionally made slow to produce after n
seconds.
tusharsamagra@Tushars-MacBook-Pro system-programming-go % python condition-variables/producer-consumer-just-mutex.py
Acquired lock in producer
Released lock in producer
Got items after 1 empty loops
[['say - woooo', "I won't let u gooo", 'say - woooo', "I won't let u gooo"]]
Time: 10
Time: 9
Time: 8
Time: 7
Time: 6
Time: 5
Time: 4
Time: 3
Time: 2
Time: 1
Acquired lock in producer
Released lock in producer
# 👉 CPU cyles wasted here in the while loop
Got items after 77431958 empty loops
[['say - woooo', "I won't let u gooo", 'say - woooo', "I won't let u gooo", 'say - woooo', "I won't let u gooo", 'say - woooo', "I won't let u gooo", 'say - woooo', "I won't let u gooo"]]
Time: 10
Time: 9
Time: 8
Time: 7
Time: 6
Time: 5
Time: 4
Time: 3
Time: 2
Time: 1
Acquired lock in producer
Released lock in producer
Got items after 77685267 empty loops
[['say - woooo', "I won't let u gooo", 'say - woooo', "I won't let u gooo"]]
producer-consumer-cond-variables.py
Here is the same code with some minor modifications to use Condition Variables
.
from threading import Thread, Lock, Condition
import time, random
# PS: Condition variables are used in conjuction with Mutexes. You can specify a mutex else Python creates one by default.
condition = Condition()
items = []
def timer(t):
while t:
time.sleep(1)
print(f"Time: {t}")
t -= 1
def producer():
while True:
# Take lock on mutex
with condition:
print("Acquired lock in producer")
items.append(["work work work"] * random.randint(5, 10))
# Send a signal
condition.notify()
print("Released lock in producer")
timer_thread = Thread(target=timer, args=(random.randint(5, 10),))
timer_thread.start()
timer_thread.join()
def consumer():
empty_loop_count = 0
while True:
# Take lock on the mutex
with condition:
if not items:
empty_loop_count += 1
# Free the lock on the mutex, and wait for signal
condition.wait()
print(f"Got items after {empty_loop_count} empty loops")
empty_loop_count = 0
print(items)
items.clear()
producer_thread = Thread(target=producer)
consumer_thread = Thread(target=consumer)
producer_thread.start()
consumer_thread.start()
Running the above program generates the following logs.
Acquired lock in producer
Released lock in producer
Got items after 0 empty loops
[['work work work', 'work work work', 'work work work', 'work work work', 'work work work', 'work work work', 'work work work']]
Time: 7
Time: 6
Time: 5
Time: 4
Time: 3
Time: 2
Time: 1
Acquired lock in producer
Released lock in producer
Got items after 1 empty loops
[['work work work', 'work work work', 'work work work', 'work work work', 'work work work', 'work work work', 'work work work', 'work work work']]
Time: 7
Time: 6
Time: 5
Time: 4
Time: 3
Time: 2
Time: 1
Acquired lock in producer
Released lock in producer
# 👉 No CPU cyles wasted here, as we wait for signal
Got items after 1 empty loops
[['work work work', 'work work work', 'work work work', 'work work work', 'work work work', 'work work work']]
As we can see, using condition variables we can pause the execution of code until we receive the signal from a producer that helps in saving a LOT of CPU cycles.