Leetcode 1114. Print in Order


The problem revolves around the use of threads in Java and ensuring a certain order of their execution. We have three threads, each one meant to call a different method. We have the Foo class that has three methods: first(), second(), and third(). Three different threads A, B, and C, are meant to call these methods. However, we need to ensure that whatever the state of the threads, or their order of execution, second() should always execute after first(), and third() after second().

Let's walk through an example:


Input: [1,2,3] - This represents the order of execution of threads Output: "firstsecondthird"

Explanation: Regardless of the input order, the output is always "firstsecondthird". This means thread A calls first(), thread B calls second(), and thread C calls third(). Even if the input order were [1,3,2], the output will still be "firstsecondthird".

Solution Approach

To solve this problem, we can use the synchronization mechanism provided by the language - mutex locks in C++, Monitor object in Java and Python, and Promises in JavaScript.

These synchronization mechanisms will ensure that the second function doesn't run until the first one is completed and the third function doesn't run until the second one is completed.

In the solution, we see that all threads are initially locked. The first() method is executed first, and upon completion, the lock on the second thread is released. This allows second() method to execute. Once the second() method is complete, the lock on the third thread is lifted, permitting third() to execute.

C++ Solution

3class Foo {
5    Foo() {
6        m1.lock();
7        m2.lock();
8    }
10    void first(function<void()> printFirst) {
11        printFirst();
12        // unlock the first mutex to let second execute
13        m1.unlock();
14    }
16    void second(function<void()> printSecond) {
17        // wait until first is done
18        m1.lock();
19        printSecond();
20        // unlock the second mutex to let third execute
21        m2.unlock();
22    }
24    void third(function<void()> printThird) {
25        // wait until second is done
26        m2.lock();
27        printThird();
28    }
31    std::mutex m1, m2;

Python Solution

3import threading
5class Foo:
6    def __init__(self):
7        self.locks = (threading.Lock(), threading.Lock())
8        self.locks[0].acquire()
9        self.locks[1].acquire()
11    def first(self, printFirst: 'Callable[[], None]') -> None:
12        printFirst()
13        self.locks[0].release()
15    def second(self, printSecond: 'Callable[[], None]') -> None:
16        with self.locks[0]:
17            printSecond()
18            self.locks[1].release()
20    def third(self, printThird: 'Callable[[], None]') -> None:
21        with self.locks[1]:
22            printThird()

Java Solution

3class Foo {
5    private Semaphore run2, run3;
7    public Foo() {
8        run2 = new Semaphore(0);
9        run3 = new Semaphore(0);
10    }
12    public void first(Runnable printFirst) throws InterruptedException {
13        printFirst.run();
14        run2.release();
15    }
17    public void second(Runnable printSecond) throws InterruptedException {
18        run2.acquire();
19        printSecond.run();
20        run3.release();
21    }
23    public void third(Runnable printThird) throws InterruptedException {
24        run3.acquire();
25        printThird.run();
26    }

JavaScript Solution

3class Foo {
4    constructor() {
5        this.promises = [Promise.resolve()];
6    }
8    first(printFirst) {
9        this.promises[0] = this.promises[0].then(() => {
10            printFirst();
11            return Promise.resolve();
12        });
13    }
15    second(printSecond) {
16        this.promises[1] = this.promises[0].then(() => {
17            printSecond();
18            return Promise.resolve();
19        });
20    }
22    third(printThird) {
23        this.promises[2] = this.promises[1].then(() => {
24            printThird();
25        });
26    }

C# Solution

3public class Foo {
4    ManualResetEvent reset1;
5    ManualResetEvent reset2;
7    public Foo() {
8        reset1 = new ManualResetEvent(false);
9        reset2 = new ManualResetEvent(false);
10    }
12    public void First(Action printFirst) {
13        printFirst();
14        reset1.Set();
15    }
17    public void Second(Action printSecond) {
18        reset1.WaitOne();
19        printSecond();
20        reset2.Set();
21    }
23    public void Third(Action printThird) {
24        reset2.WaitOne();
25        printThird();
26    }

With these solutions, we can ensure that the function execution always follows the order first(), second(), and third() regardless of the order of invocation of the threads.While the synchronization of threads can be daunting, it's necessary when the execution of different threads must occur in a specific order. The solutions provided for different programming languages are designed to implement thread synchronization in a lock-based system whereby each method 'unlocks' the next after its execution is complete.

In Python, Java, JavaScript, and C#, we use in-built synchronization features, like locks, semaphores, promises, and ManualResetEvents, to control the execution order of the threads. With Python and C#, we use the 'with' keyword to automatically handle the acquisition and release of locks.

For example, in Java we use Semaphore which is a counting semaphore used to control access to a shared resource. We can initialize Semaphore to any positive integer, where each acquire() call reduces that number, while a release() call increases it. If a thread calls acquire() when the count is 0, the thread will block until a another thread does a release().

JavaScript uses a different approach, using promises to manage the flow since JavaScript is single-threaded and asynchronous. This means that JavaScript has a different concurrency model compared with other programming languages. Serialization is ensured by chaining Promises, a construct for handling asynchronicity.

C# uses ManualResetEvent, a threading synchronization primitive that can be signaled or non-signaled. After signaling the state of ManualResetEvent, it stays in that state until a thread manually changes it. As such, the thread signaling the event doesn't need to be the one to reset it.

Implementing such solutions ensures that our code maintains correctness by ensuring that certain operations are completed before others, despite the inherent unpredictability of threads and their execution order.

While the outlined solutions are applicable for the given problem, thread synchronization is a broader concept applicable in various situations when working with concurrent processing. Therefore, understanding and mastering these basics will help tackle more complex problems regarding thread synchronization.

Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.

TA 👨‍🏫