Print in Order: Thread Synchronization with Barriers
You are given a class Foo with three methods: first(), second(), and third(). Three different threads will each call one of these methods, but they might arrive in any order. Your job is to guarantee that first() always runs before second(), and second() always runs before third(), regardless of which thread the OS schedules first.
This is LeetCode 1114: Print in Order, and it is one of the cleanest problems for learning thread synchronization. The core idea -- using signaling primitives to enforce ordering between concurrent threads -- shows up in real systems all the time.
Why this problem matters
Concurrency bugs are among the hardest to debug in production systems. Race conditions, deadlocks, and ordering violations can cause intermittent failures that are nearly impossible to reproduce. This problem strips away the complexity and gives you a pure playground for the fundamental pattern: "thread A must finish before thread B can start."
You will use this same pattern when building producer-consumer queues, coordinating multi-stage pipelines, and ensuring initialization order in concurrent applications. The primitives you learn here -- events, barriers, locks, and semaphores -- are the building blocks of every concurrent system.
The key insight
You need two synchronization gates between the three threads. Thread 2 waits at the first gate until Thread 1 signals it. Thread 3 waits at the second gate until Thread 2 signals it. Python's threading.Event is perfect for this: an event starts unset (blocking), and once you call set(), any thread waiting on it proceeds immediately.
The pattern is simple: create two events, one between each pair of consecutive functions. Each function does its work and then signals the next event. Each waiting function blocks on its event until it gets the green light.
The solution
import threading
class Foo:
def __init__(self):
self.event1 = threading.Event()
self.event2 = threading.Event()
def first(self, printFirst):
printFirst()
self.event1.set()
def second(self, printSecond):
self.event1.wait()
printSecond()
self.event2.set()
def third(self, printThird):
self.event2.wait()
printThird()
Let's trace through the logic. In __init__, we create two threading.Event objects. Both start in the unset (blocking) state.
When first() is called, it runs printFirst() immediately -- no waiting needed, because first is always allowed to go. After printing, it calls self.event1.set(), which unblocks any thread that is waiting on event1.
When second() is called, the first thing it does is self.event1.wait(). If Thread 1 has not yet finished, Thread 2 blocks here. Once event1 is set, Thread 2 proceeds, runs printSecond(), and then signals event2 for Thread 3.
When third() is called, it waits on event2. Once Thread 2 finishes and signals, Thread 3 runs printThird().
No matter what order the OS schedules the threads, the output is always "firstsecondthird".
threading.Event is ideal here because it is a one-shot signal: once set, it stays set forever. Any thread that calls wait() after the event is already set will pass through immediately with no blocking. This makes it safe even if Thread 1 finishes before Thread 2 starts.
Visual walkthrough
Let's trace through a scenario where Thread 2 and Thread 3 arrive before Thread 1. Watch how the events enforce the correct execution order.
Step 1: All threads start. Thread 2 and Thread 3 arrive first but are blocked.
event_1 and event_2 are both locked. Thread 2 waits on event_1. Thread 3 waits on event_2.
Step 2: Thread 1 runs first() and prints "first". Then it signals event_1.
Thread 1 executes printFirst(). Output so far: "first".
Step 3: event_1 is signaled. Thread 2 is unblocked.
Thread 1 is done. event_1 is now signaled. Thread 2 passes through the barrier.
Step 4: Thread 2 runs second() and prints "second". Then it signals event_2.
Thread 2 executes printSecond(). Output so far: "firstsecond".
Step 5: event_2 is signaled. Thread 3 is unblocked.
Thread 2 is done. event_2 is now signaled. Thread 3 passes through the barrier.
Step 6: Thread 3 runs third() and prints "third". All done.
Thread 3 executes printThird(). Final output: "firstsecondthird". Correct order guaranteed.
Even though Thread 2 and Thread 3 were ready to go, they had to wait for the events to be signaled in sequence. The events act as one-way gates: once opened, they stay open. This guarantees the ordering constraint no matter how the OS schedules the threads.
Complexity analysis
| Approach | Time | Space |
|---|---|---|
| Threading Events | O(1) | O(1) |
Time is O(1) per method call. Each function does a constant amount of work: one wait() call (which blocks until the signal arrives but does not loop or poll), one print, and one set() call. The total work across all three threads is constant.
Space is O(1). We store exactly two Event objects. No data structures grow with input size.
The building blocks
1. Threading Event as a one-shot gate
event = threading.Event()
event.wait()
event.set()
A threading.Event starts in the unset state. Calling wait() blocks the current thread until another thread calls set(). Once set, the event stays set permanently, so any future wait() calls return immediately. This is the right primitive when you need to say "do not proceed until some other work is done" -- exactly the constraint in this problem.
2. Chaining events for sequential ordering
def step_n(self, action, wait_event, signal_event):
wait_event.wait()
action()
signal_event.set()
The pattern generalizes to any number of steps. Each step waits on the previous event, does its work, and signals the next event. If you had five threads that needed to run in order, you would create four events and chain them the same way. The first step skips the wait (or you pre-set its event), and the last step skips the signal.
Edge cases
- Thread 1 finishes before Thread 2 starts:
event1is already set when Thread 2 callswait(), so it passes through immediately. No deadlock. - All threads arrive simultaneously: The OS picks one to run. Thread 2 and Thread 3 block on their respective events. Thread 1 runs, signals, and the chain proceeds.
- Thread 3 arrives before Thread 2: Thread 3 blocks on
event2. Even after Thread 1 signalsevent1, Thread 3 stays blocked until Thread 2 finishes and signalsevent2. - Alternative primitives: You can also solve this with
threading.Barrier,threading.Lock(acquired in__init__, released by the predecessor), orqueue.Queue. The event approach is the most readable.
From understanding to recall
You have seen how two threading.Event objects enforce sequential execution across three concurrent threads. The code is short -- five lines of logic spread across three methods. But writing it from memory under interview pressure is a different challenge.
The details that trip people up are subtle. Do you create the events in __init__ or in each method? Do you call wait() before or after the print? Do you need to clear() the event after use? (No, you do not.) These are not conceptual gaps. They are recall gaps, and they cost people offers.
Spaced repetition is how you close them. You practice writing the event initialization, the wait-then-signal chain, and the guard placement from memory at increasing intervals. After a few rounds, the pattern is automatic. You see "ensure ordering between concurrent tasks" in a problem description, and the event template flows out without hesitation.
Related posts
- Design Circular Queue - Another design problem where you build a data structure with careful state management from scratch
- Implement Queue using Stacks - Uses two underlying structures to implement ordering, similar to how events enforce ordering here
- Design HashMap - A foundational design problem that teaches you to build a data structure with clean internal state
CodeBricks breaks Print in Order into its threading event and sequential signaling building blocks, then drills them independently with spaced repetition. You type each piece from scratch until the pattern is automatic. When a concurrency problem shows up in your interview, you do not think about it. You just write it.