Threads: Scheduling, Blocking, And Realtime#

Threads Basics#

  • Primary advantage over interrupts: may block. E.g.

  • Zephyr

    • Preemptive: timesliced (much like SCHED_RR in Linux)

    • Cooperative: not timesliced (scheduling disabled); can still be interrupted by an ISR

  • One stack per thread

  • Priorities: when a higher priority thread becomes runnable, the currently running thread is suspended, and a context switch is made to run the higher priority thread

Main Thread#

  • Main thread: always there, implicitly created

  • Zephyr: runs at highest priority

  • ⟶ Careful

#include <why-thread.h>
#include <print>

int main()
{
    Why::init();

    auto mainthread = Why::Thread::self();
    std::println("priority: {}", mainthread.prioritystr());
    return 0;
}
$ sudo ./why-threads-mainthread
priority: Highest

Explicitly Created Threads#

  • Created explicitly to handle a specific task

  • Priority to distinguish from other threads

  • Zephyr: priority is mandatory

  • WhyOS: priority is optional; default Linux fair scheduling

#include <why-thread.h>
#include <print>
#include <cstdlib>

int main()
{
    Why::init();

    Why::Thread thread(
        [](){
            std::println("being scheduled, priority: {}", Why::Thread::self().prioritystr());
        }
    );
    return 0;
}
#include <why-thread.h>
#include <print>

int main()
{
    Why::init();

    Why::Thread thread(
        [](){
            std::println("being scheduled, priority: {}", Why::Thread::self().prioritystr());
        }, 
        Why::Thread::Priority::Medium
    );
    return 0;
}

Scheduling: Coordinating Multiple Threads#

  • A running thread is given only so much time (a timeslice); after that time a scheduling decision is made

  • Scheduling decision: choosing a runnable/ready thread to run on the CPU

  • Context switch: saving CPU registers of one thread, and restoring those of another

  • Potential reschedule points (i.e. when a scheduling decision is made)

    • A thread has consumed its timeslice ⟶ from potentially multiple runnable threads one must be chosen

    • A thread voluntarily relinquishes the CPU; for example by calling Why::msleep()

    • Return of an ISR

  • A thread becomes runnable/ready, when for example

    • awakes from Why::msleep()

    • a semaphore that it was waiting to take is given (by another thread or an interrupt)

    • …

  • In the moment a thread becomes runnable, and a lower priority thread has the CPU, a context switch is made in favor of the newcomer

  • Interrupts are not schedulable ⟶ are not scheduled

Attention: Blocking In Interrupt Service Routines#

  • Programming paradigm: waiting - voluntarily relinquishing the CPU

  • ISR must not wait - if they do …

    • ⟶ all sorts of undefined behavior (for performance reasons checks are not made)

    • Interrupts not arriving anymore

    • Stalling the system

  • WhyOS: interrupts are queued until the ISR returns ⟶ correct, but huge latency problem

  • Other archs/platforms may differ

Demo: Scheduling In Action#

#include <why-thread.h>
#include <why-time.h>
#include <print>

int main()
{
    Why::init();

    Why::Thread thread1(
        [](){
            for (;;)
                std::println("1");
        },
        Why::Thread::Priority::Medium
    );
    Why::Thread thread2(
        [](){
            for (;;)
                std::println("2");
        },
        Why::Thread::Priority::Medium
    );

    Why::pause();
    return 0;
}
$ sudo ./why-threads-scheduling-demo
1
2
1
1
2
...