Design a Thread-Safe Bounded Blocking Queue
LatitudeaiBackend Engineer · OnsiteLast updated
Tests your ability to design a concurrent, thread-safe bounded blocking queue using synchronization primitives, handle edge cases like spurious wakeups and timeouts, and cleanly coordinate shutdown across multiple threads.
Problem Statement
Design and implement a thread-safe bounded blocking queue for multi-producer and multi-consumer scenarios. The queue maintains a strict FIFO (First-In, First-Out) ordering and has a fixed maximum capacity.
Your class should implement the following methods:
__init__(capacity: int): Initializes the queue with a maximum capacity. RaisesValueErrorifcapacity None: Inserts an element into the queue. If the queue is full, the calling thread blocks until space becomes available, the specifiedtimeout(in seconds) expires, or the queue is closed. IftimeoutisNone, it blocks indefinitely.take(timeout: Optional[float] = None) -> Any: Removes and returns the head element of the queue. If the queue is empty, the calling thread blocks until an element is available, the specifiedtimeout(in seconds) expires, or the queue is closed. IftimeoutisNone, it blocks indefinitely.close() -> None: Closes the queue. All currently blockedputcalls must unblock and fail. Futureputcalls must fail immediately. Threads waiting intakeshould be allowed to consume any remaining elements; once the queue is empty, subsequent or currently waitingtakecalls must fail.
Define and use custom exceptions QueueClosedError and QueueTimeoutError for failed operations.
Interface
from typing import Any, Optional
class QueueClosedError(Exception):
pass
class QueueTimeoutError(Exception):
pass
class BoundedBlockingQueue:
def __init__(self, capacity: int):
pass
def put(self, item: Any, timeout: Optional[float] = None) -> None:
pass
def take(self, timeout: Optional[float] = None) -> Any:
pass
def close(self) -> None:
pass
Example Usage
q = BoundedBlockingQueue(capacity=2)
q.put(1)
q.put(2)
# q is now full; a third put without timeout would block.
q.take() # Returns 1
q.put(3) # Succeeds; queue contains [2, 3]
q.close()
# q.put(4) raises QueueClosedError
q.take() # Returns 2 (draining buffered items)
q.take() # Returns 3
# q.take() raises QueueClosedError
Constraints and Edge Cases
1 <= capacity <= 10^5.- Multiple producer and consumer threads may call methods simultaneously.
- Handle spurious wakeups correctly.
- Handle
timeout=0(immediate non-blocking check) and negative timeouts gracefully (treat negative values as non-blocking or raiseValueError). - Ensure no memory leaks or thread starvation when threads time out or when
close()is invoked.
Hints
Hint 1
Use a single reentrant or mutual exclusion lock paired with two separate condition variables (not_full and not_empty) to avoid waking up irrelevant threads.
Hint 2
Always evaluate condition predicates inside a while loop rather than an if statement to guard against spurious wakeups and state changes caused by other threads.
Hint 3
When close() is called, update a boolean flag under the lock and call notify_all() on both condition variables so all waiting threads can wake up and evaluate the termination conditions.
Follow-up questions
- How would you modify this design to reduce lock contention under very high thread counts (e.g., using a two-lock concurrent queue or lock-free circular ring buffer)?
- How can you write deterministic stress tests to detect deadlocks, race conditions, and lost wakeups without relying solely on arbitrary time.sleep() calls?
- If a thread blocked in put() or take() is interrupted or cancelled, how do you ensure the queue invariants remain consistent without leaking items or capacity permits?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.