1115. Print FooBar Alternately
Problem Description
You have a class FooBar
with two methods: foo()
and bar()
. Each method prints its respective string ("foo" or "bar") n
times in a loop.
Two separate threads will be created:
- Thread A will call the
foo()
method - Thread B will call the
bar()
method
Both threads will run concurrently on the same instance of FooBar
. Without synchronization, the output could be in any order (like "foofoofoobarbar" or "barbarfoofoo").
Your task is to modify the program using synchronization mechanisms to ensure that the output always alternates between "foo" and "bar", producing exactly "foobar" repeated n
times. For example:
- If
n = 1
, the output should be: "foobar" - If
n = 2
, the output should be: "foobarfoobar" - If
n = 3
, the output should be: "foobarfoobarfoobar"
The solution uses two semaphores to coordinate the threads:
- Semaphore
f
(initialized to 1) controls when "foo" can be printed - Semaphore
b
(initialized to 0) controls when "bar" can be printed
The synchronization works as follows:
- Thread A acquires semaphore
f
, prints "foo", then releases semaphoreb
- Thread B acquires semaphore
b
, prints "bar", then releases semaphoref
- This pattern repeats
n
times, ensuring strict alternation
The semaphores act as signals between threads - when one thread finishes printing, it signals the other thread that it's their turn to print. This guarantees the correct "foobar" pattern regardless of thread scheduling.
Intuition
When we have two threads that need to alternate their execution in a strict pattern, we need a way for them to communicate and coordinate. Think of it like two people taking turns speaking - one person needs to signal when they're done so the other knows it's their turn.
The key insight is that we need to enforce a specific ordering: "foo" must always come before "bar", and after "bar" is printed, "foo" can print again. This is a classic producer-consumer pattern where one thread produces a signal for the other to consume.
We can achieve this coordination using semaphores, which act like permission slips. A semaphore with value 1 means "you have permission to proceed," while a value of 0 means "you must wait."
Here's the reasoning:
-
Initially, we want "foo" to print first, so we give the
foo
thread permission to start (semaphoref = 1
) while making thebar
thread wait (semaphoreb = 0
). -
After "foo" prints, it needs to tell "bar" that it's now bar's turn. This is done by releasing semaphore
b
(changing it from 0 to 1), giving thebar
thread permission to proceed. -
Similarly, after "bar" prints, it needs to tell "foo" that foo can go again. This is done by releasing semaphore
f
. -
The acquire operations ensure that each thread waits for its turn -
foo
waits for semaphoref
to be available, andbar
waits for semaphoreb
to be available.
This creates a ping-pong effect: foo → signals bar → bar → signals foo → foo → signals bar... continuing for n
iterations. The semaphores essentially create a token-passing mechanism where only the thread holding the token can execute, and after execution, it passes the token to the other thread.
Solution Approach
The implementation uses two semaphores to control the execution order of the threads. Let's walk through how this works:
Initialization:
- Create semaphore
f
with initial value1
- this allows thefoo
thread to execute first - Create semaphore
b
with initial value0
- this blocks thebar
thread initially
The foo
method execution:
for _ in range(self.n):
self.f.acquire() # Wait for permission to print "foo"
printFoo() # Print "foo"
self.b.release() # Give permission for "bar" to print
self.f.acquire()
- The thread attempts to acquire semaphoref
. On the first iteration,f = 1
, so it proceeds immediately and decrementsf
to0
. On subsequent iterations, it waits until thebar
thread releasesf
.printFoo()
- Once acquired, it prints "foo"self.b.release()
- It then releases semaphoreb
, incrementing it from0
to1
, signaling thebar
thread that it can now proceed
The bar
method execution:
for _ in range(self.n):
self.b.acquire() # Wait for permission to print "bar"
printBar() # Print "bar"
self.f.release() # Give permission for "foo" to print
self.b.acquire()
- The thread attempts to acquire semaphoreb
. Initiallyb = 0
, so it blocks until thefoo
thread releasesb
. Once available, it decrementsb
to0
.printBar()
- Once acquired, it prints "bar"self.f.release()
- It then releases semaphoref
, incrementing it from0
to1
, allowing thefoo
thread to proceed with its next iteration
Execution Flow for n = 2
:
- Initial state:
f = 1
,b = 0
- Thread A (iteration 1): acquires
f
(nowf = 0
), prints "foo", releasesb
(nowb = 1
) - Thread B (iteration 1): acquires
b
(nowb = 0
), prints "bar", releasesf
(nowf = 1
) - Thread A (iteration 2): acquires
f
(nowf = 0
), prints "foo", releasesb
(nowb = 1
) - Thread B (iteration 2): acquires
b
(nowb = 0
), prints "bar", releasesf
(nowf = 1
) - Output: "foobarfoobar"
The semaphores create a strict alternation pattern where each thread must wait for the other to complete before proceeding, ensuring the output is always in the correct "foobar" order repeated n
times.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example with n = 2
to see how the semaphores coordinate the threads.
Initial Setup:
- Semaphore
f = 1
(foo can start) - Semaphore
b = 0
(bar must wait) - Thread A runs
foo()
method - Thread B runs
bar()
method
Step-by-Step Execution:
Step | Thread A (foo) | Thread B (bar) | Semaphore f | Semaphore b | Output |
---|---|---|---|---|---|
1 | f.acquire() - succeeds, f becomes 0 | b.acquire() - blocks (b is 0) | 0 | 0 | |
2 | printFoo() - prints "foo" | (still blocked) | 0 | 0 | "foo" |
3 | b.release() - b becomes 1 | (still blocked) | 0 | 1 | "foo" |
4 | Starts iteration 2: f.acquire() - blocks (f is 0) | b.acquire() - succeeds, b becomes 0 | 0 | 0 | "foo" |
5 | (still blocked) | printBar() - prints "bar" | 0 | 0 | "foobar" |
6 | (still blocked) | f.release() - f becomes 1 | 1 | 0 | "foobar" |
7 | f.acquire() - succeeds, f becomes 0 | Starts iteration 2: b.acquire() - blocks (b is 0) | 0 | 0 | "foobar" |
8 | printFoo() - prints "foo" | (still blocked) | 0 | 0 | "foobarfoo" |
9 | b.release() - b becomes 1 | (still blocked) | 0 | 1 | "foobarfoo" |
10 | Loop complete | b.acquire() - succeeds, b becomes 0 | 0 | 0 | "foobarfoo" |
11 | printBar() - prints "bar" | 0 | 0 | "foobarfoobar" | |
12 | f.release() - f becomes 1 | 1 | 0 | "foobarfoobar" |
Key Observations:
- Thread A always waits for semaphore
f
before printing "foo" - Thread B always waits for semaphore
b
before printing "bar" - After printing, each thread signals the other by releasing the opposite semaphore
- The initial values (
f=1, b=0
) ensure "foo" prints first - The alternating acquire/release pattern creates a handshake mechanism that enforces strict ordering
This synchronization guarantees that regardless of how the operating system schedules these threads, the output will always be "foobarfoobar" for n=2
.
Solution Implementation
1from threading import Semaphore
2from typing import Callable
3
4
5class FooBar:
6 """
7 A class that coordinates two threads to print "foo" and "bar" alternately.
8
9 This implementation uses semaphores to ensure that "foo" and "bar" are printed
10 in alternating order, regardless of which thread runs first or how the threads
11 are scheduled.
12 """
13
14 def __init__(self, n: int) -> None:
15 """
16 Initialize the FooBar synchronization object.
17
18 Args:
19 n: The number of times each method (foo and bar) should print.
20 """
21 self.n = n
22 # Semaphore for foo method - initially available (value=1)
23 # This allows foo to run first
24 self.foo_semaphore = Semaphore(1)
25
26 # Semaphore for bar method - initially blocked (value=0)
27 # This ensures bar waits for foo to complete first
28 self.bar_semaphore = Semaphore(0)
29
30 def foo(self, printFoo: Callable[[], None]) -> None:
31 """
32 Print "foo" n times, alternating with bar method.
33
34 Args:
35 printFoo: A callable that prints "foo" when invoked.
36 """
37 for _ in range(self.n):
38 # Wait for permission to print foo
39 # First iteration: semaphore is available (value=1)
40 # Subsequent iterations: wait for bar to release this semaphore
41 self.foo_semaphore.acquire()
42
43 # printFoo() outputs "foo". Do not change or remove this line.
44 printFoo()
45
46 # Signal bar method that foo has completed
47 # This allows bar to proceed with its print
48 self.bar_semaphore.release()
49
50 def bar(self, printBar: Callable[[], None]) -> None:
51 """
52 Print "bar" n times, alternating with foo method.
53
54 Args:
55 printBar: A callable that prints "bar" when invoked.
56 """
57 for _ in range(self.n):
58 # Wait for foo to complete before printing bar
59 # First iteration: blocked until foo releases this semaphore
60 # Subsequent iterations: wait for foo to release this semaphore
61 self.bar_semaphore.acquire()
62
63 # printBar() outputs "bar". Do not change or remove this line.
64 printBar()
65
66 # Signal foo method that bar has completed
67 # This allows foo to proceed with its next iteration
68 self.foo_semaphore.release()
69
1class FooBar {
2 private int n;
3 // Semaphore for controlling "foo" execution - initialized with 1 permit (foo goes first)
4 private Semaphore fooSemaphore = new Semaphore(1);
5 // Semaphore for controlling "bar" execution - initialized with 0 permits (bar waits)
6 private Semaphore barSemaphore = new Semaphore(0);
7
8 /**
9 * Constructor to initialize the FooBar instance with the number of iterations
10 * @param n Number of times to print "foobar"
11 */
12 public FooBar(int n) {
13 this.n = n;
14 }
15
16 /**
17 * Method executed by the "foo" thread
18 * Prints "foo" n times, alternating with bar
19 * @param printFoo Runnable that prints "foo"
20 * @throws InterruptedException if thread is interrupted while waiting
21 */
22 public void foo(Runnable printFoo) throws InterruptedException {
23 for (int i = 0; i < n; i++) {
24 // Wait for permission to print "foo" (acquire permit from fooSemaphore)
25 fooSemaphore.acquire();
26
27 // printFoo.run() outputs "foo". Do not change or remove this line.
28 printFoo.run();
29
30 // Signal that "bar" can now execute (release permit to barSemaphore)
31 barSemaphore.release();
32 }
33 }
34
35 /**
36 * Method executed by the "bar" thread
37 * Prints "bar" n times, alternating with foo
38 * @param printBar Runnable that prints "bar"
39 * @throws InterruptedException if thread is interrupted while waiting
40 */
41 public void bar(Runnable printBar) throws InterruptedException {
42 for (int i = 0; i < n; i++) {
43 // Wait for permission to print "bar" (acquire permit from barSemaphore)
44 barSemaphore.acquire();
45
46 // printBar.run() outputs "bar". Do not change or remove this line.
47 printBar.run();
48
49 // Signal that "foo" can now execute (release permit to fooSemaphore)
50 fooSemaphore.release();
51 }
52 }
53}
54
1#include <semaphore.h>
2#include <functional>
3
4class FooBar {
5private:
6 int n; // Number of times to print "foobar"
7 sem_t semaphoreFoo; // Semaphore to control foo() execution
8 sem_t semaphoreBar; // Semaphore to control bar() execution
9
10public:
11 FooBar(int n) {
12 this->n = n;
13 // Initialize semaphoreFoo with value 1 (foo goes first)
14 sem_init(&semaphoreFoo, 0, 1);
15 // Initialize semaphoreBar with value 0 (bar waits initially)
16 sem_init(&semaphoreBar, 0, 0);
17 }
18
19 void foo(std::function<void()> printFoo) {
20 for (int i = 0; i < n; i++) {
21 // Wait for permission to print "foo"
22 sem_wait(&semaphoreFoo);
23
24 // printFoo() outputs "foo". Do not change or remove this line.
25 printFoo();
26
27 // Signal bar() that it can now print "bar"
28 sem_post(&semaphoreBar);
29 }
30 }
31
32 void bar(std::function<void()> printBar) {
33 for (int i = 0; i < n; i++) {
34 // Wait for foo() to complete before printing "bar"
35 sem_wait(&semaphoreBar);
36
37 // printBar() outputs "bar". Do not change or remove this line.
38 printBar();
39
40 // Signal foo() that it can print "foo" again
41 sem_post(&semaphoreFoo);
42 }
43 }
44};
45
1// Number of times to print "foobar"
2let n: number;
3
4// Semaphore-like structures using Promises
5let semaphoreFoo: Promise<void>;
6let semaphoreBar: Promise<void>;
7let resolveFoo: (() => void) | null = null;
8let resolveBar: (() => void) | null = null;
9
10/**
11 * Initializes the synchronization mechanism
12 * @param count - Number of times to print "foobar"
13 */
14function initializeFooBar(count: number): void {
15 n = count;
16
17 // Initialize semaphoreFoo with immediate resolution (foo goes first)
18 semaphoreFoo = Promise.resolve();
19
20 // Initialize semaphoreBar with pending promise (bar waits initially)
21 semaphoreBar = new Promise<void>((resolve) => {
22 resolveBar = resolve;
23 });
24}
25
26/**
27 * Prints "foo" n times, alternating with bar()
28 * @param printFoo - Callback function that prints "foo"
29 */
30async function foo(printFoo: () => void): Promise<void> {
31 for (let i = 0; i < n; i++) {
32 // Wait for permission to print "foo"
33 await semaphoreFoo;
34
35 // Create new promise for next iteration of foo
36 semaphoreFoo = new Promise<void>((resolve) => {
37 resolveFoo = resolve;
38 });
39
40 // printFoo() outputs "foo". Do not change or remove this line.
41 printFoo();
42
43 // Signal bar() that it can now print "bar"
44 if (resolveBar) {
45 resolveBar();
46 }
47 }
48}
49
50/**
51 * Prints "bar" n times, alternating with foo()
52 * @param printBar - Callback function that prints "bar"
53 */
54async function bar(printBar: () => void): Promise<void> {
55 for (let i = 0; i < n; i++) {
56 // Wait for foo() to complete before printing "bar"
57 await semaphoreBar;
58
59 // Create new promise for next iteration of bar
60 semaphoreBar = new Promise<void>((resolve) => {
61 resolveBar = resolve;
62 });
63
64 // printBar() outputs "bar". Do not change or remove this line.
65 printBar();
66
67 // Signal foo() that it can print "foo" again
68 if (resolveFoo) {
69 resolveFoo();
70 }
71 }
72}
73
Time and Space Complexity
The time complexity is O(n)
, where n
is the parameter passed to the constructor. This is because both the foo
and bar
methods execute their loops exactly n
times. Each iteration performs a constant amount of work - acquiring a semaphore, calling a print function, and releasing a semaphore are all O(1)
operations. Therefore, the total time complexity is O(n) * O(1) = O(n)
.
The space complexity is O(1)
. The class only uses a fixed amount of extra space regardless of the input size n
. It stores:
- One integer variable
self.n
- Two semaphore objects
self.f
andself.b
The semaphores themselves use constant space as they only maintain internal state (counter and queue) that doesn't grow with n
. The loop variables in both methods also use constant space. Since the space usage doesn't scale with the input size, the space complexity is O(1)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Semaphore Initialization Values
One of the most common mistakes is initializing both semaphores with the same value or swapping their initial values. This can lead to deadlock or incorrect output ordering.
Incorrect Implementation:
def __init__(self, n: int) -> None:
self.n = n
# Wrong: Both initialized to 1
self.foo_semaphore = Semaphore(1)
self.bar_semaphore = Semaphore(1) # Should be 0!
Problem: If both semaphores start at 1, both threads can execute simultaneously on their first iteration, potentially producing "barfoo" instead of "foobar".
Another Incorrect Version:
def __init__(self, n: int) -> None:
self.n = n
# Wrong: Swapped initial values
self.foo_semaphore = Semaphore(0) # Should be 1!
self.bar_semaphore = Semaphore(1) # Should be 0!
Problem: This causes "bar" to print first, resulting in "barfoo" pattern instead of "foobar".
2. Swapping acquire() and release() Operations
Another critical mistake is confusing which semaphore to acquire versus which to release in each method.
Incorrect Implementation:
def foo(self, printFoo: Callable[[], None]) -> None:
for _ in range(self.n):
self.bar_semaphore.acquire() # Wrong semaphore!
printFoo()
self.foo_semaphore.release() # Wrong semaphore!
Problem: This creates a deadlock immediately. The foo thread tries to acquire a semaphore that starts at 0, blocking forever since only the bar thread can release it, but bar is also waiting.
3. Using a Single Semaphore
Some might attempt to use just one semaphore, thinking it's simpler.
Incorrect Implementation:
def __init__(self, n: int) -> None:
self.n = n
self.semaphore = Semaphore(1)
def foo(self, printFoo: Callable[[], None]) -> None:
for _ in range(self.n):
self.semaphore.acquire()
printFoo()
self.semaphore.release()
def bar(self, printBar: Callable[[], None]) -> None:
for _ in range(self.n):
self.semaphore.acquire()
printBar()
self.semaphore.release()
Problem: A single semaphore only provides mutual exclusion but doesn't enforce ordering. The output could be "foofoobarbar" or "barbarfoofoo" or any other combination.
4. Missing the Loop Structure
Forgetting that each method needs to execute n times, not just once.
Incorrect Implementation:
def foo(self, printFoo: Callable[[], None]) -> None:
# Missing loop - only executes once!
self.foo_semaphore.acquire()
printFoo()
self.bar_semaphore.release()
Problem: This would only print "foobar" once regardless of the value of n.
5. Race Condition in Initialization
While less common, attempting to use other synchronization primitives incorrectly:
Incorrect Implementation Using Lock:
def __init__(self, n: int) -> None:
self.n = n
self.lock = threading.Lock()
self.turn = "foo"
def foo(self, printFoo: Callable[[], None]) -> None:
for _ in range(self.n):
while True:
with self.lock:
if self.turn == "foo":
printFoo()
self.turn = "bar"
break
# Busy waiting - inefficient!
def bar(self, printBar: Callable[[], None]) -> None:
for _ in range(self.n):
while True:
with self.lock:
if self.turn == "bar":
printBar()
self.turn = "foo"
break
# Busy waiting - inefficient!
Problem: While this might work, it uses busy-waiting (spinning), which wastes CPU cycles. Semaphores are more efficient as they block the thread properly.
Correct Solution Reminder:
The correct approach uses two semaphores with specific initial values:
foo_semaphore
starts at 1 (allowing foo to go first)bar_semaphore
starts at 0 (forcing bar to wait)- Each method acquires its own semaphore and releases the other's semaphore
- This creates a ping-pong effect ensuring strict alternation
Which of the following shows the order of node visit in a Breadth-first Search?
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!