Before we can go deeper into details, one of the basic concepts of any RTOS should be explained – task states and transitions between them. As these phrases include words “state” and “transition”, the best way to describe them accurately is a state diagram. It so happens that there’s one below.
“created” state
This is the state of the task when it is created but not started yet. A task in “created” state will never occupy any CPU time. When you start the task, it transitions from “created” to “started” state. After that there is no way to return to “created” state.
Assuming that an example – just as a picture – is worth a thousand words, here’s how that can look in distortos:
void threadFunction(); auto thread = distortos::makeStaticThread<1024>(1, threadFunction); assert(thread.getState() == distortos::ThreadState::created);
void threadFunction(); auto thread = distortos::makeDynamicThread({1024, 1}, threadFunction); assert(thread.getState() == distortos::ThreadState::created);
A thread with static (automatic) or dynamic storage is created with 1024 bytes of stack, very low priority (1) and bound to some threadFunction()
. All checks of created thread’s state will return “created” as long as no one starts it.
“runnable” state
This is one of the states in which the task will be for most of its lifetime. Only tasks in “runnable” state can use the CPU. The decision whether a “runnable” task is actually given a chance to run is taken by the scheduling algorithm employed in the system. There are two possible transitions from “runnable” state:
- to “blocked” state, when some blocking function is executed,
- to “terminated” state when the task returns from its main function.
Interesting fact – when a task reads its own state, it will always be “runnable”.
As previously – lets show that with an example:
void veryLongFunction(); auto thread = distortos::makeStaticThread<1024>(1, veryLongFunction); thread.start(); assert(thread.getState() == distortos::ThreadState::runnable);
void veryLongFunction(); auto thread = distortos::makeDynamicThread({1024, 1}, veryLongFunction); thread.start(); assert(thread.getState() == distortos::ThreadState::runnable);
After creating and starting a thread bound to a veryLongFunction()
, we expect that all checks of created thread’s state will return “runnable” as long as this function is not yet finished – so probably for a “very long” time.
“blocked” state
When the task cannot progress any further – for example because it waits for some external event, like a press of a button – it will enter “blocked” state. When a task is “blocked” it will not use any CPU time. The only possible transition from this state – back to “runnable” state – happens when the task gets unblocked – for example by the external event it was waiting for or by a timeout.
It is worth noting that in majority of cases the task will enter “blocked” state as a result of its own actions:
- task may explicitly block itself for a while by calling
distortos::ThisThread::sleepFor(std::chrono::minutes{5})
, - task can request a “resource” that is currently not available, for example trying to lock a mutex which is already locked by another task,
- it may wait for an external event, like data received via serial port,
- …
How about a strange example?
distortos::Semaphore semaphore {0}; auto thread = distortos::makeStaticThread<1024>(1, &distortos::Semaphore::wait, std::ref(semaphore)); thread.start(); distortos::ThisThread::sleepFor(std::chrono::minutes{5}); assert(thread.getState() == distortos::ThreadState::blockedOnSemaphore);
distortos::Semaphore semaphore {0}; auto thread = distortos::makeDynamicThread({1024, 1}, &distortos::Semaphore::wait, std::ref(semaphore)); thread.start(); distortos::ThisThread::sleepFor(std::chrono::minutes{5}); assert(thread.getState() == distortos::ThreadState::blockedOnSemaphore);
This code creates and starts a thread bound directly to a blocking Semaphore::wait()
function. Then the parent thread takes a 5 minutes long nap, giving CPU to the created thread. As the semaphore’s value is 0, created thread is expected to block immediately the moment it starts and remain in that state until the semaphore is posted. When parent thread wakes up, created thread will still be blocked – assuming no one fiddled with the semaphore while parent thread was sleeping.
“terminated” state
When the task returns from its main function for whatever reason – it finished its job, fatal error was detected, it just didn’t have the right mood to continue, … – it enters final “terminated” state. As there is nothing more to do, the task will not consume any CPU time. The only thing that the system can do is to dispose of all resources allocated for the task. This can be done only from another task, most likely the one that created the now “terminated” task, but this is not obligatory.
In an embedded system tasks may as well never reach this state, because they are frequently written as endless loops which never return. This is perfectly fine, the system doesn’t care whether it will execute the same tasks until the end of times or a completely new set every 10 seconds.
Here’s another code snippet for distortos:
void veryShortFunction(); auto thread = distortos::makeStaticThread<1024>(1, veryShortFunction); thread.start(); distortos::ThisThread::sleepFor(std::chrono::seconds{42}); assert(thread.getState() == distortos::ThreadState::terminated);
void veryShortFunction(); auto thread = distortos::makeDynamicThread({1024, 1}, veryShortFunction); thread.start(); distortos::ThisThread::sleepFor(std::chrono::seconds{42}); assert(thread.getState() == distortos::ThreadState::terminated);
As in all previous cases we create a thread with 1024 bytes of stack, very low priority (1), bounded to a supposedly short function veryShortFunction()
. After that this freshly created thread is started and the parent thread sleeps for 42 seconds. When it wakes up, the veryShortFunction()
should already by done (it wouldn’t be very short otherwise), so the created thread is already “terminated”.
Variations of task states
The diagram and the previous four chapters are just a brief look at what you can expect in the majority of systems. Described states and their transitions are not the only possible/valid/correct options – there can be more or less, but most of combinations can be reduced to the one presented above.
Some systems don’t need “created” state, as their API allows you only to create a task and start it at the same time. POSIX systems and C++11’s native <thread>
are in this group – if you create a thread with pthread_create()
or construct a std::thread
object, they are started immediately, effectively skipping “created” state. This is also possible in distortos, by creating the thread with distortos::makeAndStartStaticThread()
or distortos::makeAndStartDynamicThread()
. This can be quite useful, as such objects can be completely hidden in their own translation units.
In some systems task’s function is not allowed to return – it must be implemented as an infinite loop. In that case there’s no need for “terminated” state.
There may be more states as well. For example in distortos “blocked” state is divided into multiple sub-states, depending on the reason for blocking – there’s “blocked on semaphore”, “blocked on mutex”, … . In some systems there’s a distinction between a task in “runnable” state and a task that is actually running at the moment.
Other transitions than the ones shown on the diagram may also be possible. Some systems may support the concept of restarting the task that is already terminated. Some others may allow external termination of the task that is blocked.