Instead of using single priority queue for all tasks, split into using
"run queue", which represents tasks not waiting until specific time,
which should be run on every (well, next) loop iteration, and wait queue,
still a priority queue. Run queue is a simple FIFO, implemented by
ucollections.deque, recently introduced in pfalcon/micropython. Thus,
there's minimal storage overhead and intrinsic scheduling fairness.
Generally, run queue should hold both a callback/coro and its arguments,
but as we don't feed any send args into coros still, it's optimized to
hold just 1 items for coros, while 2 for callbacks.
Introducing run queue will also allow to get rid of tie-breaking counter
in utimeq implementation, which was introduced to enforce fair scheduling.
It's no longer needed, as all tasks which should be run at given time
are batch-removed from wait queue and batch-inserted into run queue. So,
they may be executed not in the order scheduled (due to non-stable order
of heap), but the whole batch will be executed "atomically", and any new
schedulings from will be processed no earlier than next loop iteration.
This also adds CancelledError exception and makes TimeoutError be a
subclass of it. As well as adds default exception handler for it in
the eventloop (which just skips re-adding this coro to the scheduling
queue, as expected).
Coros which removed from normal scheduling queue (and possibly put into
another queue, like I/O queue here) are marked with .pend_throw(False).
If wait_for() cancels such a coro, it is explicitly scheduled for execution,
so they actually could process pending exception (coro's exception handler
should take care of removing it from another queue and related clean up).
Currently executed task is a top-level coroutine scheduled in the event
loop (note that sub-coroutines aren't scheduled in the event loop and
are executed implicitly by yield from/await, driven by top-level coro).
yield False won't reschedule current coroutine to be run again. This is
useful when coro is put on some waiting queue (and is similar to what
yield IORead/yield IOWrite do).
If there is a coroutine to run immediately (with wait delay <= 0),
uasyncio.core never called .wait() method, which is required to
process I/O events (and schedule coroutines waiting for them). So
now, call .wait(0) even if there's a coroutine to run immediately.
uasyncio uses different timebase than CPython's asyncio, so absolute
time scheduling compatible with it is impossible. Instead, there's
call_at_() which schedules using modular millisecond time.
wait() may finish prematurely due to I/O completion, and schedule new,
earlier than before tasks to run. So, after call to wait(), we need to
check current time and time of head task, and continue to wait if needed.
Recently introduced provisional utimeq.peektime() is used to optimize
querying time of a head task.
Specifically, that a coroutine scheduled to run at some time (after some
delay) waits requested time before it's run and not run prematurely in
case an I/O completion happens before it.