A thread is the basic unit of runnable code. The vast majority of firmware code will run in threads, whether it’s a user-defined thread, a thread created by the RTOS (for example, a system workqueue thread), or a thread created by an RTOS subsystem (for example, a logger module), or a thread created by a library (for example, AT Monitor library). A thread has the following items:
Thread control block: This is of type k_thread. For each thread, there will be an instance of a thread control block within the RTOS that keeps track of a thread’s information, specifically its metadata.
Stack: Each thread will have its own stack. The stack area’s size must be set to align with the specific processing requirements of the thread. The next section will cover how to set the right thread size.
Entry point function: This is the body of the thread or, in other words, the functionality implemented by it. It usually contains an infinite loop, as exiting the entry point will terminate the thread. The entry point function can have three optional argument values that can be passed to it on start.
Thread priority: The priority is just a signed integer that governs the “type” of the thread. It instructs the scheduler how to allocate CPU time to the thread. We will dive into this in the next topic, Scheduler in-depth.
Optional thread options: As covered in the nRF Connect SDK Fundamentals course – Lesson 7, by using this optional field, you can make the thread receive special treatment under specific circumstances.
Optional starting delay: You can instruct the kernel to immediately place the created thread in the queue of ready threads (ready queue) by passing K_NO_WAIT which is simply a start delay of 0. Or we can specify an optional start delay.
Threads are created using either the K_THREAD_DEFINE() macro or the k_thread_create() function. In both cases, a stack needs to be allocated statically (dynamic threads are not supported in Zephyr RTOS as of V3.4.0). The K_THREAD_DEFINE() macro manages the stack allocation itself, and the desired stack size is passed as a parameter to the same macro that was covered in the nRF Connect SDK Fundamentals course. Conversely, if you use the k_thread_create() function, you must allocate a stack using the K_THREAD_STACK_DEFINE() macro in advance.
As was covered in the fundamentals course, when creating a thread, you can start it immediately or after specifying a certain delay. Once the thread is started, it is placed in the queue of ready threads (ready queue).
Note
There is also the option to create a thread with the delay set to K_FOREVER, which effectively makes the thread inactive. To activate, call k_thread_start(), which will add the thread to the queue of ready threads (ready queue).
Definition
The ready queue is the queue of threads that has the state Ready. The scheduler only cares about the threads in the ready queue when deciding which one should be the current Running thread.
If the scheduler picks up the thread for execution, its state transitions to Running. Scheduling policies are covered in the next topic, Scheduling in-depth. The thread will stay Running until:
The thread changes to an Unready state, meaning it has either the Sleeping, Suspended, or Waiting state.
Sleeping: The thread decides to sleep for some time by calling k_sleep() or its derivatives.
Suspended: Another thread suspends the thread by calling k_thread_suspend().
Waiting: The thread waits for a kernel object (for example, a mutex or a semaphore) that is unavailable.
The thread yields on its own or is preempted by the scheduler.
The thread yields by calling k_yield() to give up the CPU, and putting itself at the end of the ready queue.
The thread is preempted by the scheduler in a rescheduling point when there is a higher priority thread in the ready queue. When preempted, the thread is placed at the end of the ready queue.
Terminate or abort
The thread execution ends by termination or aborting.
A termination takes place when the entry point function of the thread is exited. This happens in a few situations where a thread has defined non-repetitive tasks, and it’s done with those tasks.
Aborting can happen automatically if the thread encounters a fatal error condition, such as de-referencing a null pointer, in which case the RTOS aborts the thread. Alternatively, a thread can be deliberately aborted by another thread or by itself using the k_thread_abort() function.
More details on threads can be found on the Threads page of the Zephyr Project documentation.