Katkat

Design a Thread-Safe Bounded Blocking Queue

LatitudeaiBackend Engineer · Onsite

Last 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. Raises ValueError if capacity None: Inserts an element into the queue. If the queue is full, the calling thread blocks until space becomes available, the specified timeout (in seconds) expires, or the queue is closed. If timeout is None, 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 specified timeout (in seconds) expires, or the queue is closed. If timeout is None, it blocks indefinitely.
  • close() -> None: Closes the queue. All currently blocked put calls must unblock and fail. Future put calls must fail immediately. Threads waiting in take should be allowed to consume any remaining elements; once the queue is empty, subsequent or currently waiting take calls 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 raise ValueError).
  • 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?