1114. Print in Order
Problem Description
You have a class Foo
with three methods: first()
, second()
, and third()
. Each method prints its respective name ("first", "second", or "third").
The challenge is that the same instance of Foo
will be shared among three different threads:
- Thread A will call
first()
- Thread B will call
second()
- Thread C will call
third()
Your task is to design a synchronization mechanism to ensure that these methods are always executed in the correct order, regardless of how the operating system schedules the threads. Specifically:
second()
must execute only afterfirst()
has completedthird()
must execute only aftersecond()
has completed
The solution uses two locks (l2
and l3
) that are initially acquired in the constructor. The first()
method can run immediately and releases l2
when done, allowing second()
to proceed. The second()
method waits to acquire l2
, then releases l3
when done, allowing third()
to proceed. The third()
method waits to acquire l3
before executing.
This ensures the output is always "first" → "second" → "third", even if the threads are scheduled to run in any order by the operating system.
Intuition
The key insight is that we need to create dependencies between the threads - each thread should wait for the previous one to complete before it can proceed. Think of it like a relay race where each runner can only start when they receive the baton from the previous runner.
Since we don't control when threads are scheduled by the operating system, we need synchronization primitives to enforce ordering. The natural choice is to use locks or semaphores as "gates" that control when each method can execute.
The pattern we're looking for is:
first()
should run immediately (no waiting)second()
should wait untilfirst()
signals it's donethird()
should wait untilsecond()
signals it's done
This suggests we need two synchronization points - one between first()
and second()
, and another between second()
and third()
. We can implement this with two locks that act as barriers:
- Lock
l2
blockssecond()
from running untilfirst()
releases it - Lock
l3
blocksthird()
from running untilsecond()
releases it
By acquiring both locks in the constructor, we ensure that second()
and third()
will block when they try to acquire their respective locks. Then each method releases the lock for the next method in sequence, creating a chain of execution: first()
→ releases l2
→ second()
can run → releases l3
→ third()
can run.
This elegant solution uses the lock's mutual exclusion property not for protecting shared data, but as a signaling mechanism to coordinate thread execution order.
Solution Approach
The solution uses a lock-based synchronization mechanism to enforce the execution order of the three methods. Let's walk through the implementation step by step:
Initialization:
def __init__(self):
self.l2 = threading.Lock()
self.l3 = threading.Lock()
self.l2.acquire()
self.l3.acquire()
We create two locks (l2
and l3
) and immediately acquire them. This is crucial - by acquiring these locks in the constructor, we're setting up barriers that will block second()
and third()
from executing until the appropriate locks are released.
Method Execution Flow:
-
first()
method:def first(self, printFirst: 'Callable[[], None]') -> None: printFirst() self.l2.release()
- Executes immediately without any blocking (no lock acquisition needed)
- After printing "first", it releases lock
l2
- This signals that
second()
can now proceed
-
second()
method:def second(self, printSecond: 'Callable[[], None]') -> None: self.l2.acquire() printSecond() self.l3.release()
- Attempts to acquire lock
l2
(will block untilfirst()
releases it) - Once acquired, prints "second"
- Releases lock
l3
to allowthird()
to proceed
- Attempts to acquire lock
-
third()
method:def third(self, printThird: 'Callable[[], None]') -> None: self.l3.acquire() printThird()
- Attempts to acquire lock
l3
(will block untilsecond()
releases it) - Once acquired, prints "third"
- Attempts to acquire lock
Why This Works:
The pattern creates a dependency chain through lock acquisition and release:
l2
acts as a gate betweenfirst()
andsecond()
l3
acts as a gate betweensecond()
andthird()
No matter which order the threads are scheduled by the operating system, the locks ensure the correct execution sequence. For example, if Thread C (calling third()
) starts first, it will immediately block on l3.acquire()
and wait until Thread B releases that lock, which in turn can only happen after Thread A releases l2
.
Alternative Approach with Semaphores:
As mentioned in the reference, we could also use semaphores with initial counts:
- Semaphore
a
with count 1 (allowsfirst()
to run immediately) - Semaphore
b
with count 0 (blockssecond()
initially) - Semaphore
c
with count 0 (blocksthird()
initially)
The semaphore approach follows the same signaling pattern but uses counting semaphores instead of binary locks, providing essentially the same synchronization behavior.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example where three threads are scheduled in the worst-case order to demonstrate how the lock mechanism ensures correct execution.
Scenario: Threads scheduled in reverse order (C → B → A)
Initial State:
l2
is acquired (locked)l3
is acquired (locked)- OS schedules threads: Thread C starts first, then B, then A
Step 1: Thread C starts and calls third()
Thread C: third() → tries to acquire l3 → BLOCKED (l3 is locked)
Thread C waits...
Step 2: Thread B starts and calls second()
Thread B: second() → tries to acquire l2 → BLOCKED (l2 is locked)
Thread B waits...
Thread C still waiting...
Step 3: Thread A starts and calls first()
Thread A: first() → executes printFirst() immediately → prints "first"
Thread A: releases l2
Thread B: wakes up! Acquires l2 successfully
Thread C still waiting...
Step 4: Thread B continues
Thread B: now has l2 → executes printSecond() → prints "second"
Thread B: releases l3
Thread C: wakes up! Acquires l3 successfully
Step 5: Thread C continues
Thread C: now has l3 → executes printThird() → prints "third"
Final Output: "first" → "second" → "third"
Even though the OS scheduled the threads in completely reverse order (C, B, A), the lock mechanism enforced the correct execution sequence. Thread C and B had to wait at their respective lock acquisition points until the previous methods in the sequence completed and released the appropriate locks. This demonstrates how the solution handles any possible thread scheduling order.
Solution Implementation
1import threading
2from typing import Callable
3
4class Foo:
5 def __init__(self):
6 # Initialize two locks to control the execution order
7 # Lock for controlling when second() can execute
8 self.lock_for_second = threading.Lock()
9 # Lock for controlling when third() can execute
10 self.lock_for_third = threading.Lock()
11
12 # Pre-acquire both locks to block second() and third() initially
13 # This ensures first() must complete before second() can run
14 self.lock_for_second.acquire()
15 # This ensures second() must complete before third() can run
16 self.lock_for_third.acquire()
17
18 def first(self, printFirst: Callable[[], None]) -> None:
19 # First method can execute immediately without waiting
20 # Execute the first print function
21 printFirst()
22
23 # Release the lock to allow second() to proceed
24 self.lock_for_second.release()
25
26 def second(self, printSecond: Callable[[], None]) -> None:
27 # Wait until first() releases the lock
28 self.lock_for_second.acquire()
29
30 # Execute the second print function
31 printSecond()
32
33 # Release the lock to allow third() to proceed
34 self.lock_for_third.release()
35
36 def third(self, printThird: Callable[[], None]) -> None:
37 # Wait until second() releases the lock
38 self.lock_for_third.acquire()
39
40 # Execute the third print function
41 printThird()
42
43 # No need to release as this is the final method
44
1class Foo {
2 // Semaphore for controlling execution of first() method
3 // Initialized with 1 permit to allow first() to run immediately
4 private Semaphore firstSemaphore = new Semaphore(1);
5
6 // Semaphore for controlling execution of second() method
7 // Initialized with 0 permits to block second() until first() completes
8 private Semaphore secondSemaphore = new Semaphore(0);
9
10 // Semaphore for controlling execution of third() method
11 // Initialized with 0 permits to block third() until second() completes
12 private Semaphore thirdSemaphore = new Semaphore(0);
13
14 /**
15 * Constructor for Foo class
16 */
17 public Foo() {
18 // Empty constructor - semaphores are initialized at declaration
19 }
20
21 /**
22 * Executes the first print operation
23 * This method will run first regardless of thread execution order
24 *
25 * @param printFirst Runnable that prints "first"
26 * @throws InterruptedException if thread is interrupted while waiting
27 */
28 public void first(Runnable printFirst) throws InterruptedException {
29 // Acquire permit from firstSemaphore (will succeed immediately on first call)
30 firstSemaphore.acquire();
31
32 // printFirst.run() outputs "first". Do not change or remove this line.
33 printFirst.run();
34
35 // Release permit for secondSemaphore, allowing second() to proceed
36 secondSemaphore.release();
37 }
38
39 /**
40 * Executes the second print operation
41 * This method will only run after first() has completed
42 *
43 * @param printSecond Runnable that prints "second"
44 * @throws InterruptedException if thread is interrupted while waiting
45 */
46 public void second(Runnable printSecond) throws InterruptedException {
47 // Acquire permit from secondSemaphore (blocks until first() releases it)
48 secondSemaphore.acquire();
49
50 // printSecond.run() outputs "second". Do not change or remove this line.
51 printSecond.run();
52
53 // Release permit for thirdSemaphore, allowing third() to proceed
54 thirdSemaphore.release();
55 }
56
57 /**
58 * Executes the third print operation
59 * This method will only run after second() has completed
60 *
61 * @param printThird Runnable that prints "third"
62 * @throws InterruptedException if thread is interrupted while waiting
63 */
64 public void third(Runnable printThird) throws InterruptedException {
65 // Acquire permit from thirdSemaphore (blocks until second() releases it)
66 thirdSemaphore.acquire();
67
68 // printThird.run() outputs "third". Do not change or remove this line.
69 printThird.run();
70
71 // Release permit for firstSemaphore, resetting for potential reuse
72 firstSemaphore.release();
73 }
74}
75
1class Foo {
2private:
3 std::mutex mutex_for_second; // Controls access to second() method
4 std::mutex mutex_for_third; // Controls access to third() method
5
6public:
7 Foo() {
8 // Initially lock both mutexes to prevent second() and third()
9 // from executing before first()
10 mutex_for_second.lock();
11 mutex_for_third.lock();
12 }
13
14 void first(std::function<void()> printFirst) {
15 // Execute the first function immediately (no waiting needed)
16 printFirst();
17
18 // Unlock mutex to allow second() to proceed
19 mutex_for_second.unlock();
20 }
21
22 void second(std::function<void()> printSecond) {
23 // Wait until first() completes (mutex unlocked by first())
24 mutex_for_second.lock();
25
26 // Execute the second function
27 printSecond();
28
29 // Unlock mutex to allow third() to proceed
30 mutex_for_third.unlock();
31 }
32
33 void third(std::function<void()> printThird) {
34 // Wait until second() completes (mutex unlocked by second())
35 mutex_for_third.lock();
36
37 // Execute the third function
38 printThird();
39
40 // Note: mutex_for_third remains locked after execution
41 // This is acceptable since third() is the final method
42 }
43};
44
1// Mutex-like locks for controlling method execution order
2let isSecondUnlocked: boolean = false; // Controls access to second() method
3let isThirdUnlocked: boolean = false; // Controls access to third() method
4
5// Promise-based synchronization primitives
6let secondPromiseResolve: (() => void) | null = null;
7let thirdPromiseResolve: (() => void) | null = null;
8
9// Create promises that will be resolved when the respective methods can proceed
10const secondPromise = new Promise<void>((resolve) => {
11 secondPromiseResolve = resolve;
12});
13
14const thirdPromise = new Promise<void>((resolve) => {
15 thirdPromiseResolve = resolve;
16});
17
18/**
19 * Executes the first function immediately and unlocks second()
20 * @param printFirst - Function to be executed first
21 */
22async function first(printFirst: () => void): Promise<void> {
23 // Execute the first function immediately (no waiting needed)
24 printFirst();
25
26 // Unlock mutex to allow second() to proceed
27 isSecondUnlocked = true;
28 if (secondPromiseResolve) {
29 secondPromiseResolve();
30 }
31}
32
33/**
34 * Waits for first() to complete, then executes the second function and unlocks third()
35 * @param printSecond - Function to be executed second
36 */
37async function second(printSecond: () => void): Promise<void> {
38 // Wait until first() completes (mutex unlocked by first())
39 if (!isSecondUnlocked) {
40 await secondPromise;
41 }
42
43 // Execute the second function
44 printSecond();
45
46 // Unlock mutex to allow third() to proceed
47 isThirdUnlocked = true;
48 if (thirdPromiseResolve) {
49 thirdPromiseResolve();
50 }
51}
52
53/**
54 * Waits for second() to complete, then executes the third function
55 * @param printThird - Function to be executed third
56 */
57async function third(printThird: () => void): Promise<void> {
58 // Wait until second() completes (mutex unlocked by second())
59 if (!isThirdUnlocked) {
60 await thirdPromise;
61 }
62
63 // Execute the third function
64 printThird();
65
66 // Note: isThirdUnlocked remains true after execution
67 // This is acceptable since third() is the final method
68}
69
Time and Space Complexity
The time complexity is O(1)
and the space complexity is O(1)
.
Time Complexity Analysis:
- The
__init__
method performs a constant number of operations: creating two locks and acquiring them, which are allO(1)
operations. - The
first
method executesprintFirst()
and releases a lock, bothO(1)
operations. - The
second
method acquires a lock, executesprintSecond()
, and releases another lock, allO(1)
operations. - The
third
method acquires a lock and executesprintThird()
, bothO(1)
operations. - Each method performs a fixed number of operations regardless of input size, resulting in
O(1)
time complexity.
Space Complexity Analysis:
- The class uses exactly two
threading.Lock
objects (self.l2
andself.l3
), which occupy a constant amount of memory. - No additional data structures are created that scale with input size.
- The space usage remains constant regardless of how many times the methods are called, resulting in
O(1)
space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Lock Not Released on Exception
One critical pitfall in this implementation is that if an exception occurs in printFirst()
or printSecond()
, the subsequent locks won't be released, causing permanent deadlock for the waiting threads.
Problem Example:
def first(self, printFirst: Callable[[], None]) -> None:
printFirst() # If this throws an exception...
self.lock_for_second.release() # This never executes!
Solution: Use try-finally blocks to ensure locks are always released:
def first(self, printFirst: Callable[[], None]) -> None:
try:
printFirst()
finally:
self.lock_for_second.release()
def second(self, printSecond: Callable[[], None]) -> None:
self.lock_for_second.acquire()
try:
printSecond()
finally:
self.lock_for_third.release()
2. Reusability Issue
The current implementation can only be used once. After all three methods execute, the locks remain in their final state (both released), so subsequent calls won't maintain the ordering constraint.
Problem Scenario:
foo = Foo()
# First round works fine
foo.first(lambda: print("first"))
foo.second(lambda: print("second"))
foo.third(lambda: print("third"))
# Second round - all methods can run immediately!
foo.third(lambda: print("third")) # Runs without waiting
foo.first(lambda: print("first")) # Order is broken
Solution: Reset the locks after each complete cycle or use a different synchronization primitive like barriers or condition variables that can be reset:
class ReusableFoo:
def __init__(self):
self.first_done = threading.Event()
self.second_done = threading.Event()
def first(self, printFirst):
printFirst()
self.first_done.set()
def second(self, printSecond):
self.first_done.wait()
printSecond()
self.second_done.set()
def third(self, printThird):
self.second_done.wait()
printThird()
# Reset for next round
self.first_done.clear()
self.second_done.clear()
3. Resource Leak with Acquired but Unreleased Locks
If the Foo
object is destroyed or goes out of scope while threads are still waiting on locks, those threads may hang indefinitely.
Solution: Implement proper cleanup in a destructor or context manager:
def __del__(self):
# Ensure locks are released if object is destroyed
try:
self.lock_for_second.release()
except:
pass
try:
self.lock_for_third.release()
except:
pass
4. Confusing Lock Names
The original variable names l2
and l3
are unclear and can lead to maintenance issues. Using descriptive names like lock_for_second
and lock_for_third
makes the code's intent clearer and reduces the chance of errors during modifications.
Given a sorted array of integers and an integer called target, find the element that
equals to the target and return its index. Select the correct code that fills the
___
in the given code snippet.
1def binary_search(arr, target):
2 left, right = 0, len(arr) - 1
3 while left ___ right:
4 mid = (left + right) // 2
5 if arr[mid] == target:
6 return mid
7 if arr[mid] < target:
8 ___ = mid + 1
9 else:
10 ___ = mid - 1
11 return -1
12
1public static int binarySearch(int[] arr, int target) {
2 int left = 0;
3 int right = arr.length - 1;
4
5 while (left ___ right) {
6 int mid = left + (right - left) / 2;
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
1function binarySearch(arr, target) {
2 let left = 0;
3 let right = arr.length - 1;
4
5 while (left ___ right) {
6 let mid = left + Math.trunc((right - left) / 2);
7 if (arr[mid] == target) return mid;
8 if (arr[mid] < target) {
9 ___ = mid + 1;
10 } else {
11 ___ = mid - 1;
12 }
13 }
14 return -1;
15}
16
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!