~/problems / Coordination / Condition variable

Delayed task scheduler

hard 2 levels ~50 min ConfluentPinterest

Level 1 One dispatcher thread

Build DelayedScheduler, a small in-process timer service. Callers hand it a function and a delay; the scheduler calls the function later, on its own background thread, while callers carry on.

  • DelayedScheduler(): start the scheduler and its background thread (make it a daemon thread, so a scheduler nobody shuts down can't keep the program alive).
  • schedule(task, delay_ms) -> int: arrange for task() (a function with no arguments) to be called once, no earlier than delay_ms milliseconds after this call. Return immediately with the task's ID: 1, 2, 3, ... in the order schedule is called. A delay of 0 (or a negative one) means "as soon as possible".
  • pending() -> int: how many scheduled tasks haven't started yet.
  • shutdown(wait=True) -> None: stop accepting tasks; afterwards schedule raises RuntimeError.
    • wait=True: every pending task still runs at its due time; shutdown returns once they have all finished and the background thread has exited.
    • wait=False: pending tasks are dropped and never run; a task that is already running is allowed to finish; then the thread exits and shutdown returns.
    • Calling shutdown again does nothing.

Ordering: tasks run one at a time on the single background thread, in order of their due time (time of schedule call + delay); equal due times run in ID order. A task may be late (if an earlier task is still running) but never early.

s = DelayedScheduler()
log = []
s.schedule(lambda: log.append("slow"), 300)   # 1
s.schedule(lambda: log.append("fast"), 50)    # 2
s.schedule(lambda: log.append("mid"), 150)    # 3
s.pending()                                   # 3
s.shutdown(wait=True)                         # returns after ~300 ms
log                                           # ["fast", "mid", "slow"]

The trap: after the thread starts sleeping until the task that's due first, a new, earlier task may arrive. The thread must wake up and re-plan, otherwise a 50 ms task scheduled just after a 10-second one would wait 10 seconds. Don't poll in a loop with sleep either.

A task may itself call schedule. Never hold your lock while running a task.

Hint: The background thread should never sleep past the earliest due time it knows about, and must re-check the heap whenever it wakes, because it may have been woken by an earlier arrival rather than by its timeout.

Show hint

Keep the pending tasks in a heap of (due_time, id, task) (using time.monotonic()), guarded by one threading.Condition that schedule notifies after each push.

Level 2 unlocks when level 1 passes.

Topic: Condition variable. wait_for(predicate) + notify_all; always include a termination case.

0:00
Ctrl ' run · Ctrl ↵ submit
esc