When working with multiple processes in Python, you may run into situations where different parts of your program try to use or change the same resource—like a file, a number, or a list. This can lead to unexpected results if those processes clash while working at the same time. This is where synchronization comes in.
One basic way to keep things safe is by using a Lock
. A Lock
acts like a door: only one process can go through at a time. If another process comes along, it must wait until the door is open again. This makes sure that only one process touches the shared resource at a time.
In this article, we’ll explore how to use multiprocessing.Lock
in a variety of hands-on situations. Each example is built around a simple, real-world idea to make the concept easy to follow and enjoyable. By the end, you’ll have a clear understanding of how Lock
works to keep shared data safe when multiple processes run side by side.
Creating and Using a Lock
A Lock
in Python acts like a gate that only one process can pass through at a time. When a process reaches a critical section—some code that uses a shared resource—it needs to “acquire” the lock before entering. If another process already holds the lock, the new one waits until it’s free. After the work is done, the lock is “released” so the next process can enter.
Python also lets you use the with
statement with a Lock
. This is a shortcut that automatically calls acquire()
at the start and release()
at the end, keeping your code cleaner and safer.
from multiprocessing import Process, Lock
import time
# This function represents a chef trying to use the oven
def bake(name, lock):
print(f"{name} wants to use the oven.")
# Only one chef can enter this block at a time
with lock:
print(f"{name} is baking...")
time.sleep(2) # Simulate time spent baking
print(f"{name} is done baking.")
if __name__ == "__main__":
lock = Lock() # Create a lock to control oven access
chefs = ["Mary", "Lois"] # Two chefs wanting to bake
# Start a separate process for each chef
for name in chefs:
Process(target=bake, args=(name, lock)).start()
In this example, both Mary and Lois want to use the same oven. Since the oven can only handle one baker at a time, we use a Lock
to control access. When Mary starts baking, Lois has to wait until she’s done. Using with lock
keeps the code clean and ensures the lock is released even if something goes wrong. This is a simple way to avoid clashes when working with shared tools or spaces in multiprocessing programs.
Lock in Loops
Sometimes, processes perform repeated actions that involve shared data. In such cases, using a Lock
inside the loop ensures that only one process updates the shared resource at a time, even during fast, repeated access. Without the lock, race conditions can happen—one process might read or write while another is mid-operation, causing incorrect results.
from multiprocessing import Process, Value, Lock
# Each squirrel adds nuts to the shared pile
def collect_nuts(name, nut_pile, lock):
for _ in range(5):
with lock: # Lock access for this update
nut_pile.value += 1
print(f"{name} added a nut. Total: {nut_pile.value}")
if __name__ == "__main__":
nut_pile = Value('i', 0) # Shared integer to store total nuts
lock = Lock() # Lock to guard the shared value
names = ["George", "Fred", "Hermione"] # Squirrel names
# Start one process per squirrel
for name in names:
Process(target=collect_nuts, args=(name, nut_pile, lock)).start()
In this example, George, Fred, and Hermione are all busy adding nuts to the same pile. The Value
is a special shared integer used by all the processes. Each squirrel adds one nut at a time, five times in a loop. The Lock
makes sure that when one squirrel updates the total, the others wait their turn. Without it, two squirrels might try to update the pile at once, causing the count to be off. With the lock in place, every nut is counted properly.
Lock Around Shared Data Structures
When multiple processes need to update a shared data structure—like a list—you can use multiprocessing.Manager
to create that shared object. But just like with simple values, you still need to protect it. If two processes try to change the list at the same time, data might be lost or mixed up. A Lock
makes sure that changes happen one at a time.
from multiprocessing import Process, Manager, Lock
import time
# Each journalist writes a timestamped headline to the shared list
def write_headline(name, headlines, lock):
headline = f"{name} reports at {time.strftime('%H:%M:%S')}"
with lock: # Only one journalist can write at a time
headlines.append(headline)
print(f"{name} added a headline.")
if __name__ == "__main__":
with Manager() as manager:
headlines = manager.list() # Shared list managed by a Manager
lock = Lock() # Lock to protect the list
names = ["Harry", "Ron", "Hermione"]
processes = []
for name in names:
p = Process(target=write_headline, args=(name, headlines, lock))
processes.append(p)
p.start()
# Wait for all journalists to finish
for p in processes:
p.join()
# Print all the collected headlines
print("\nAll headlines:")
for line in headlines:
print(" -", line)
In this newsroom example, each journalist (Harry, Ron, and Hermione) writes a headline with a timestamp into a shared list. We use Manager().list()
so that the list can be accessed safely by all processes. However, to keep the list from getting scrambled when multiple journalists write at the same time, we wrap each append()
call with a Lock
. This ensures every headline is added cleanly, one after the other.
Using Lock with Conditional Logic
Sometimes you need to not only update a shared value but also check it first. If the check and update happen separately, two processes might both think the condition is true and act on it at the same time. A Lock
lets you safely check and update the state all in one protected step.
from multiprocessing import Process, Value, Lock
# Each buyer tries to get a ticket
def buy_ticket(name, seats, lock):
with lock: # Check and update are both done while holding the lock
if seats.value > 0:
seats.value -= 1
print(f"{name} bought a ticket. Tickets left: {seats.value}")
else:
print(f"{name} found no tickets left.")
if __name__ == "__main__":
seats = Value('i', 3) # Start with 3 available tickets
lock = Lock() # Lock to guard the shared ticket counter
buyers = ["Samantha", "Amber", "Heather", "Cherish"] # 4 people trying to buy
# Start one process per buyer
for name in buyers:
Process(target=buy_ticket, args=(name, seats, lock)).start()
In this ticket counter example, we begin with only three tickets. Four buyers rush in to get one. Without the lock, two or more buyers might check the ticket count at the same time, see that one is left, and both try to take it—causing wrong results. The Lock
makes sure each process checks and updates the count alone, ensuring only three tickets are sold, and the rest get the correct “sold out” message.
Using Built-in Locks with Value
and Array
When working with shared Value
or Array
objects, you don’t always need a separate Lock()
. These objects come with a built-in lock you can access through .get_lock()
. This keeps your code simpler when protecting just that shared object.
Dogs sharing a water bowl (Value
)
Here, several dogs try to drink water from the same bowl. The bowl’s water count is a shared Value
guarded by its built-in lock to ensure only one dog drinks at a time.
from multiprocessing import Process, Value
def drink(name, water_level):
with water_level.get_lock(): # Use built-in lock to protect shared value
if water_level.value > 0:
water_level.value -= 1
print(f"{name} drank water. Water left: {water_level.value}")
else:
print(f"{name} found the bowl empty.")
if __name__ == "__main__":
water_level = Value('i', 2) # Two drinks available
dogs = ["Brian", "Joe", "Chris"]
for name in dogs:
Process(target=drink, args=(name, water_level)).start()
This example uses the Value
’s built-in lock to safely update the water level. Each dog tries to drink, but the lock makes sure no two dogs reduce the water count at the same time.
Birds placing seeds in spots (Array
)
Now, several birds place seeds into a shared array representing seed spots. The entire array is locked during each write to prevent overlapping writes.
from multiprocessing import Process, Array
def place_seed(index, seed_box):
with seed_box.get_lock(): # Lock whole array during write
seed_box[index] = index + 1
print(f"Bird {index} placed a seed at spot {index}.")
if __name__ == "__main__":
seed_box = Array('i', 5) # Five empty seed spots
for i in range(5):
Process(target=place_seed, args=(i, seed_box)).start()
Here, the built-in lock on the Array
ensures each bird’s write happens alone, preventing two birds from writing to the array at once. This keeps the shared data consistent without needing extra locks.
Conclusion
In this article, we saw how Lock
helps manage access to shared resources in multiprocessing. We used Lock
to protect shared numbers, lists, and even conditional checks, making sure only one process changes data at a time. The with lock
statement made the code clean and easy by handling lock acquire and release automatically.
Now, you can try using Lock
in your own fun projects — like treasure hunts where players take turns, library book checkouts, or feeding schedules for pets. These simple examples show how powerful and useful Lock
is for coordinating multiple processes safely.