Facebook Pixel

1114. Print in Order

EasyConcurrency
Leetcode Link

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:

  1. second() must execute only after first() has completed
  2. third() must execute only after second() 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 until first() signals it's done
  • third() should wait until second() 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:

  1. Lock l2 blocks second() from running until first() releases it
  2. Lock l3 blocks third() from running until second() 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 l2second() can run → releases l3third() 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:

  1. 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
  2. second() method:

    def second(self, printSecond: 'Callable[[], None]') -> None:
        self.l2.acquire()
        printSecond()
        self.l3.release()
    • Attempts to acquire lock l2 (will block until first() releases it)
    • Once acquired, prints "second"
    • Releases lock l3 to allow third() to proceed
  3. third() method:

    def third(self, printThird: 'Callable[[], None]') -> None:
        self.l3.acquire()
        printThird()
    • Attempts to acquire lock l3 (will block until second() releases it)
    • Once acquired, prints "third"

Why This Works:

The pattern creates a dependency chain through lock acquisition and release:

  • l2 acts as a gate between first() and second()
  • l3 acts as a gate between second() and third()

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 (allows first() to run immediately)
  • Semaphore b with count 0 (blocks second() initially)
  • Semaphore c with count 0 (blocks third() 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 Evaluator

Example 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 l2executes printSecond()prints "second"
Thread B: releases l3
Thread C: wakes up! Acquires l3 successfully

Step 5: Thread C continues

Thread C: now has l3executes 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 all O(1) operations.
  • The first method executes printFirst() and releases a lock, both O(1) operations.
  • The second method acquires a lock, executes printSecond(), and releases another lock, all O(1) operations.
  • The third method acquires a lock and executes printThird(), both O(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 and self.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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More