Thread states and the run queue
At any moment a thread is in one of a few states. It is running while a CPU executes it, ready when it could run but no CPU is free for it, and blocked while it waits for something: a lock, a device, a signal, or a moment in time, which some kernels track as a separate sleeping state. A new thread starts out created and becomes ready when it is started, and a finished one is dead until the kernel reclaims it. Only ready threads compete for the CPU, and the scheduler keeps them in its run queue. The name is traditional: the structure can be a FIFO, a list sorted by some key, a heap, or one queue per priority level, whatever makes choosing the next thread cheap for the policy.
A CPU always has to run something, so when no thread is ready it runs an idle thread, which usually halts the core until the next interrupt. An idle thread should never block, because nothing stands behind it to fall back to.
The trap is the running thread. Whether it stays in the run queue while it runs, or is taken out when picked and put back when it leaves the CPU, is a convention, and code that mixes the two conventions either queues a thread twice or loses it. A thread queued twice gets two turns, and in a sorted queue a stale entry that keeps its place breaks the order for every thread behind it. A thread lost from the queue stays ready without ever being picked, which from outside looks like a hang.
In Cosmos the states are those of SchedulerThreadState: Created, Ready, Running, Blocked, Sleeping and Dead, and a policy never changes them. The running thread is normally not in the run structure: PickNext removes the thread it returns, and the thread leaving the CPU comes back through OnThreadYield after the pick, only if it was still Running. The exception is a thread that blocked and was woken before the switch took it off the CPU, which is Ready and queued while it still runs. A wake for a thread already Ready or Running does nothing, but a Created thread keeps that state until it first runs, so OnThreadReady can arrive twice for it and has to guard against a second insert. The idle thread is the boot thread itself, the one that goes on to call the kernel's BeforeRun and Run. PickNext returning null runs it, whatever state it is in. The kernel's mutex, interrupt event and condition variable, the last one behind CoreLib's own waits (Thread.Sleep, Thread.Join, a WaitHandle) through its low-level monitor, make it spin, poll or halt between checks instead of parking, so it never reaches OnThreadBlocked: a parked idle thread would come back through the fallback as Running without the wake that returns its tickets. Otherwise it passes through the hooks like any other thread: it reaches OnThreadCreate already Running when a policy is installed, and comes back through OnThreadYield when it is switched out. See Lifecycle and The interface.