I saw this code no long ago

lock = threading.Lock()
# some code
if some_condition:
    with lock:
        if some_condition:
            # some code

The same condition checked twice before and after the lock aquired and released. It took me a while to think through this, and you’ll probably see something simular if you do coding with threads. Let me explain why is it.

Imagine in a world, there are only Tom and Jerry

Tom = threading.Thread(target=take_that_CAKE, args=["Tom"])
Jerry = threading.Thread(target=take_that_CAKE, args=["Jerry"])

In case you knew threads in general, but not in Python, the parameter target is just the function to execute, and the args are the parameters for that target. Now, they both love having cheese cakes, so I’ll make the take_that_CAKE function.

def take_that_CAKE(who):
	while True:
		if saw_a_cheese_cake():
			time.sleep(1) # it takes 1 second to relize that is a cheese cake
			with raise_hand_to_grab_it:
				eat_the_cheese_cake()
				print(who, "ate the cheese cake!, now there is", cheese_cake, "cake")

basically, what Tom and Jerry does, is keep searching for the cheese cake, and if they see one, they grab and eat the cheese cake. Let me show you the saw_a_cheese_cake() and eat_the_cheese_cake()

cheese_cake = 1 # piece
raise_hand_to_grab_it = threading.Lock()

def saw_a_cheese_cake():
	global cheese_cake
	return cheese_cake == 1

def eat_the_cheese_cake():
	global cheese_cake
	cheese_cake -= 1

One last element to introduce, the raise_hand_to_grab_it is a lock. It ensures only one person gets the cake.

Let’s see the whole playground file

import threading
import time

raise_hand_to_grab_it = threading.Lock()

cheese_cake = 1 # piece

def saw_a_cheese_cake():
	global cheese_cake
	return cheese_cake == 1

def eat_the_cheese_cake():
	global cheese_cake
	cheese_cake -= 1

def take_that_CAKE(who):
	while True:
		if saw_a_cheese_cake():
			time.sleep(1) # take 1 second to relize that is a cheese cake
			with raise_hand_to_grab_it:
				eat_the_cheese_cake()
				print(who, "ate the cheese cake!, now there is", cheese_cake, "cake")

Tom = threading.Thread(target=take_that_CAKE, args=["Tom"])
Jerry = threading.Thread(target=take_that_CAKE, args=["Jerry"])

# adding these lines to run Tom and Jerry
Jerry.start()
Tom.start()

# the process wait for the threads to be finished
Jerry.join()
Tom.join()

Now, save it as tom_and_jerry.py and run it, what happens if you run the code above?

$ py tom_and_jerry.py
Tom ate the cheese cake!, now there is 0 cake
Jerry ate the cheese cake!, now there is -1 cake

You see the problem now? both Tom and Jerry saw the cake, and they both grab and eat the same cake twice!

Why is that? well, whoever saw the cake first, will sleep for 1 second, the system then decide to run the other Thread. Now, with two threads both see a cheese cake, they sleep, and they grab it no matter the cake still exist or not. One way to prevent it is to add another if statement after the with so that the second thread won’t grab an empty cake.

def take_that_CAKE(who):
	while True:
		if saw_a_cheese_cake():
			time.sleep(1) # take 1 second to relize that is a cheese cake
			with raise_hand_to_grab_it:
        if saw_a_cheese_cake():
					eat_the_cheese_cake()
					print(who, "ate the cheese cake!, now there is", cheese_cake, "cake")

That’s it, simple but effective method of handling the problem!