When working with multiple threads, there are times when you want all threads to pause at a certain point and wait for each other before moving on. This ensures that no thread gets too far ahead, and everyone stays in sync.
A Barrier in Python threading acts like a red light on the road. Each thread must stop and wait at the barrier until all the other threads have arrived. Once everyone is ready, the barrier lifts, and all threads continue together.
Barriers are very useful when threads need to coordinate their work, especially when each thread handles a part of the same overall task. Using a barrier helps keep the threads working smoothly as a team.
What is a Barrier?
A Barrier is a synchronization tool that makes threads wait for each other at a certain point in the program. Each thread calls barrier.wait()
, and if not all threads have arrived, it pauses there.
Once the set number of threads (called the barrier’s “parties”) have all reached the barrier, they are all released at the same time and can continue their work together.
Think of it like waiting for all your friends to arrive before starting a group game — no one starts early, and everyone begins together. This helps threads stay coordinated and in step.
Creating a Barrier
To create a barrier, use threading.Barrier(n)
, where n
is the number of threads that need to meet at the barrier before any can continue.
For example:
import threading
barrier = threading.Barrier(4) # Waits for 4 threads
This means the barrier will stay closed, making threads wait, until all 4 threads have called barrier.wait()
. Only then will all threads be released to continue together.
How to Use a Barrier
Each thread calls barrier.wait()
to pause and wait for the others. When the last thread arrives at the barrier, all threads are released and continue at the same time.
import threading
barrier = threading.Barrier(
3,
lambda: print('We are all here...') # Called after all threads have entered the barrier.
)
def adventurer(name):
print(f"{name} arrives at the gate.")
barrier.wait()
print(f"{name} enters the dungeon!")
names = ["Knight", "Mage", "Archer"]
for name in names:
threading.Thread(target=adventurer, args=(name,)).start()
In this example, the three adventurers arrive at the gate and wait for each other. Once all have arrived, they enter the dungeon together — just like a team starting a quest all at once.
Barrier with Work Before and After
Threads can do some work first, then wait at the barrier for others, and finally continue together.
import threading
import time
import random
barrier = threading.Barrier(3)
def team_member(name):
prep = random.randint(1, 3)
print(f"{name} is getting ready (takes {prep}s)...")
time.sleep(prep)
print(f"{name} is ready and waiting.")
barrier.wait()
print(f"{name} starts the task!")
members = ["Alice", "Bob", "Charlie"]
for m in members:
threading.Thread(target=team_member, args=(m,)).start()
Here, each team member takes some random time to prepare. They all wait at the barrier until everyone is ready, then they start the task together — just like a well-coordinated team launching a project at the same moment.
Barrier with Action Callback
A Barrier can take a special function called an action. This function runs once right after all threads arrive, but before they continue.
import threading
import time
import random
def start_signal():
print("All ready! Launching now!")
barrier = threading.Barrier(3, action=start_signal)
def team_member(name):
prep = random.randint(1, 3)
print(f"{name} is preparing (takes {prep}s)...")
time.sleep(prep)
print(f"{name} is ready and waiting.")
barrier.wait()
print(f"{name} starts the task!")
members = ["Alice", "Bob", "Charlie"]
for m in members:
threading.Thread(target=team_member, args=(m,)).start()
What happens: When all team members reach the barrier, the start_signal
function runs once, printing a message. Then, all threads proceed together. It’s like a coach giving a shout just before the team bursts into action!
Resetting a Barrier
Sometimes, you want threads to wait at the barrier multiple times, like in rounds or phases. You can use barrier.reset()
to clear the barrier so it can be used again for the next wait.
Imagine a team doing two rounds of a group task. After the first round, we reset the barrier so they can start fresh for the next round.
import threading
import time
barrier = threading.Barrier(3)
def worker(name):
print(f"{name} starting round 1")
barrier.wait()
print(f"{name} finished round 1")
if name == "Worker-0": # Only one thread resets the barrier
print("Resetting barrier for round 2...")
barrier.reset()
time.sleep(1) # Wait a bit to make sure reset happens
print(f"{name} starting round 2")
try:
barrier.wait()
print(f"{name} finished round 2")
except threading.BrokenBarrierError:
print(f"{name} barrier was reset early, handling gracefully")
threads = [threading.Thread(target=worker, args=(f"Worker-{i}",)) for i in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
This example shows how threads wait at the barrier for the first round and then continue together. Before the second round starts, one thread resets the barrier to prepare for a fresh wait. The threads then wait again at the barrier for round two. If the barrier is reset too early, some threads may get a BrokenBarrierError
, so it’s important to handle this error properly to keep the program running smoothly.
Real-World Example: Relay Race with Barrier Synchronization
Here’s a fun real-world example: Imagine a relay race where runners wait for all team members to finish one lap before starting the next.
import threading
import time
import random
# Barrier for 3 runners
barrier = threading.Barrier(3)
def runner(name, laps):
for lap in range(1, laps + 1):
prep = random.uniform(0.5, 1.5)
print(f"{name} running lap {lap} (takes {prep:.2f}s)...")
time.sleep(prep)
print(f"{name} finished lap {lap}, waiting for teammates...")
# Wait for all runners to finish the lap
barrier.wait()
if lap < laps:
print(f"{name} ready for next lap!\n")
else:
print(f"{name} completed all laps!\n")
def race():
threads = []
for runner_name in ["Runner-1", "Runner-2", "Runner-3"]:
t = threading.Thread(target=runner, args=(runner_name, 3))
threads.append(t)
t.start()
for t in threads:
t.join()
race()
Each runner runs 3 laps. After finishing a lap, every runner waits at the barrier for the others to catch up. When all runners arrive, the barrier opens, and they all start the next lap together.
This process repeats naturally without calling reset()
manually. The barrier resets itself after releasing all threads, ready for the next wait. It’s a smooth way to sync threads in repeated steps.
This pattern works perfectly for repeated phases where all threads must sync before continuing.
Summary Table
Feature | Description |
---|---|
Barrier(n) | Waits for n threads to arrive |
wait() | Called by each thread to pause at barrier |
action= | Optional function run when all arrive |
reset() | Resets the barrier to start over |
Best Practices
Always make sure the number of threads matches the barrier’s limit. If fewer threads reach the barrier, they will wait forever, causing your program to hang. This keeps things running smoothly and avoids deadlocks.
Use barriers mainly to synchronize different phases or steps of a task. They are not meant to control access to shared resources — for that, tools like locks or semaphores are better choices.
The action callback is a handy feature. Use it to run any setup or signal right before all threads continue. This helps keep your code clean and your thread coordination clear.
Conclusion
Barriers help threads move forward together by making sure everyone waits until all are ready. They are perfect when you need tasks to line up at a checkpoint before continuing.
You can use barriers in games, group simulations, or any team-based logic where synchronization matters. Give them a try to make your threaded programs work smoothly as a team.