Implement a multithreaded task executor with semaphores
Company: SoFi
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Quick Answer: This question evaluates understanding of concurrent programming, thread-safety, semaphore-style resource management, exception-safe permit release, and extensible API design, and it falls under the Coding & Algorithms category testing concurrency/multithreading, synchronization primitives, and resource management.
Constraints
- 0 <= len(tasks) <= 200000
- 1 <= max_threads <= 10^9
- For every task, 1 <= permits <= max_threads
- 0 <= duration <= 10^9
- `kind` is either `'M'` or `'D'`
- For divide tasks with `value != 0`, assume `value` divides `input_value` exactly
Examples
Input: (3, [('M', 2, 2, 5, 4), ('M', 5, 1, 4, 7), ('D', 3, 2, 2, 18), ('D', 0, 1, 1, 9), ('D', 3, 3, 2, 27)])
Expected Output: [(0, 5, None), (0, 4, None), (5, 7, 6), (7, 8, 'ERROR'), (8, 10, 9)]
Explanation: The first two multiply tasks start immediately at time 0 and consume all 3 permits. The first divide task must wait until time 5 to get 2 permits. The divide-by-zero task returns 'ERROR' but still releases its permit at time 8, allowing the final task to start.
Input: (5, [])
Expected Output: []
Explanation: No tasks are submitted, so the schedule is empty.
Input: (4, [('M', 2, 2, 3, 5), ('M', 3, 2, 3, 5), ('D', 5, 4, 2, 20)])
Expected Output: [(0, 3, None), (0, 3, None), (3, 5, 4)]
Explanation: Both multiply tasks run from time 0 to 3 and together use all 4 permits. The divide task can only start once both finish at time 3.
Input: (3, [('M', -2, 1, 4, 7), ('D', 2, 2, 3, -8), ('D', 7, 3, 1, 21)])
Expected Output: [(0, 4, None), (0, 3, -4), (4, 5, 3)]
Explanation: The blocking divide task can run alongside the first multiply task because enough permits are free. The last task needs all 3 permits, so it must wait until the multiply task finishes at time 4.
Input: (2, [('M', 10, 2, 0, 3), ('D', 5, 2, 1, 20)])
Expected Output: [(0, 0, None), (0, 1, 4)]
Explanation: The zero-duration multiply task starts and finishes at time 0, immediately freeing both permits for the divide task.
Hints
- Keep a min-heap of currently running tasks ordered by finish time. When you need more permits, jump time forward to the next finishing task and release all tasks that end then.
- The caller's current time only advances in two situations: when waiting to acquire permits, or when a blocking divide task is running. Fire-and-forget multiply tasks do not advance the caller's time.