Multi-tasking under Zephyr OS
Introduction
Multi-tasking Programs
In this codelab, we will learn basic principle for creating multi-tasking application using Zephyr RTOS, namely:
- How to define threads for specific tasks of a program.
- How to synchronize multiple threads and control access to shared resources.
- Study the influence of shared resources on task scheduling.
Ultimately, we will transform the BikeComputer program into a multi-tasking concurrent application.
What you’ll build
In this codelab, you’re going to program simple programs using various multi-tasking primitives. Programs will be presented as exercices that run independently from each other.
What you’ll learn
- How to use threads for performing different tasks in a program.
- How to share resources among different threads.
- How to synchronize tasks based on resources or events, including interrupt-based events.
- What the influence of shared resources can have on task scheduling.
What you’ll need
- Zephyr Development Environment for developing and debugging your program in C++.
- The BikeComputer part 1 codelab and the BikeComputer part 2 codelab are prerequisites to this codelab.
Threads in Zephyr RTOS
Zephyr RTOS runs on single-microcontroller embedded systems. In this context, a thread is an independent segment of a program that executes within the single process running on the microcontroller. Threading enables multiple tasks to run concurrently using a scheduler. It is important to point out that, while threads provide developers with flexibility, they require resources for themselves and the scheduler.
Your application (main function) starts execution in the main thread, but it’s not the only thread running in Zephyr RTOS. When starting an Zephyr RTOS application, there are other threads running system services:
- Main: The default thread that executes the application’s main function.
The main thread has 1KiB of stack space by default. The application can
configure it in “prj.conf” by defining the
MAIN_STACK_SIZEparameter, as follows:
CONFIG_MAIN_STACK_SIZE=2046 // stack size in bytes
-
Idle: The scheduler runs this thread when there is no other activity in the system (for example, when all other threads are waiting for an event). This ensures that the program does not burn empty processor cycles and is put to sleep for as long as possible.
-
Logging: The thread that’s run to log all information. It is is created only when
CONFIG_LOGis enabled.
Using the Utils Helper Class for Logging Thread Info
The Utils class is available in the
zpp_lib
repository. You may use this class in your BikeSystem implementation to log
statistics related to each thread running in your application.
If you add a call to the zpp_lib::Utils::logThreadsSummary() method at the start of the
BikeComputer::start() method, you may observe the three different threads,
as shown below:
Thread statistics logging
00:00:00.252,166] <inf> zpp_rtos: ---+----------------+------------+-----------+------+-------------------
[00:00:00.252,502] <inf> zpp_rtos: 1 | 0x20000170 | logging | queued | 14 | 48 / 768
[00:00:00.252,593] <inf> zpp_rtos: 2 | 0x20000848 | idle | ready | 15 | 48 / 320
[00:00:00.253,082] <inf> zpp_rtos: 3 | 0x20000918 | main | queued | 11 | 704 / 4096
[00:00:00.253,082] <inf> zpp_rtos: ---+----------------+------------+-----------+------+-------------------
Zephyr RTOS Scheduling Principles
Under Zephyr RTOS, threads are either cooperative or preemptible threads:
- Cooperative threads run until they either complete their task or perform an action that renders them unready. Under Zephyr RTOS, these threads have a negative priority value.
- Preemptible threads may become the running thread and may be preempted by a cooperative thread or another preemptible thread with a higher or equal priority. Under Zephyr RTOS, these threads have a non-negative priority value.
Scheduling Based on Priority
For the purposes of this lecture, we will only use preemptible threads. With these threads, the scheduler selects the highest-priority ready thread to be the current running thread. When there are multiple threads with the same priority that are ready, the scheduler chooses the one that has been waiting the longest. Thread priorities are established at creation time and they may be modified using the zpp_lib::Thread::setPriority() method.
With Zephyr RTOS, the queue of ready threads can be implemented using different mechanisms. The choice depends on various criteria. However, given the limited number of threads we will use, the simplest mechanism, CONFIG_SCHED_SIMPLE, is appropriate.
Preemptive Time Slicing
Once a preemptive thread becomes the current thread, it remains so until a thread with a higher priority becomes ready or the current thread performs an action that renders it unready. Consequently, if a preemptive thread performs lengthy computations, it may cause unacceptable delays in scheduling other threads, including those with the same priority. This is the reason why most schedulers, including the Zephyr RTOS scheduler, implement round-robin or time slicing. This allows threads with the same priority to run concurrently.
In time slicing, the scheduler divides time into time slices. At the end of each time slice, the scheduler checks if there are other threads with the same priority as the current one. If so, it preempts the current preemptible thread and dispatches another one.
Under Zephyr RTOS, time slicing can be configured as follows:
-
CONFIG_TIMESLICINGenables or disables time slicing among preemptible threads of equal priority. Time slicing is enabled by default. -
CONFIG_TIMESLICE_SIZEspecifies the amount of time (in milliseconds) that a thread can execute before being preempted by a thread of equal priority. The default value is 20 milliseconds. -
CONFIG_TIMESLICE_PRIORITYspecifies the thread priority level at which time slicing takes effect. Time slicing is not applied on threads with a higher priority than this ceiling. The default ceiling value is 0.
Priorities Values
It is important to recall that with Zephyr RTOS, higher priority means a smaller priority value. This is made explicit in zpp_lib, where priorities are defined as enum class PreemptableThreadPriority.
Main Thread Default Priority
The main thread is created by the system with a default priority that can be configured using CONFIG_MAIN_THREAD_PRIORITY. The default value is 0, meaning that by default the main thread has the highest priority among preemptible threads. Using zpp_lib, the default main thread priority must be configured because zpp_lib::PreemptableThreadPriority::PriorityRealtime has a value that is higher than 0 in any case. The default value should be set to zpp_lib::PreemptableThreadPriority::PriorityNormal, which means a value of CONFIG_NUM_PREEMPT_PRIORITIES - 4 (by default 11).
Using zpp_lib::Events For Waiting For an Event
It is very common in a multi-tasking environment to wait for a specific event to
happen before performing a given task. An obvious example is waiting for a user
to press for a button. For this purpose, Zephyr RTOS provides the Events
API, that is encapsulated in the zpp_lib::Events class.
For demonstrating the use of this API, we create a WaitOnButton class defined as follows:
wait_on_button.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file main.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Declaration of the WaitOnButton class
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#pragma once
// stl
#include <chrono>
// zpp_lib
#include "zpp_include/events.hpp"
#include "zpp_include/interrupt_in.hpp"
#include "zpp_include/thread.hpp"
namespace multi_tasking {
class WaitOnButton {
public:
explicit WaitOnButton(const char* threadName);
[[nodiscard]] zpp_lib::ZephyrResult start();
void wait_started();
void wait_exit();
private:
void waitForButtonEvent();
void buttonPressed();
static constexpr uint8_t kPressedEvent = BIT(0);
static constexpr uint8_t kStartedEvent = BIT(1);
zpp_lib::Thread _thread;
std::chrono::microseconds _pressedTime;
zpp_lib::Events _events;
zpp_lib::InterruptIn<zpp_lib::PinName::BUTTON1> _pushButton;
};
} // namespace multi_tasking
wait_on_button.cpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file main.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Implementation of the WaitOnButton class
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#include "wait_on_button.hpp"
// zephyr
#include <zephyr/logging/log.h>
// zpp_lib
#include "zpp_include/time.hpp"
LOG_MODULE_REGISTER(wait_on_button, CONFIG_APP_LOG_LEVEL);
namespace multi_tasking {
WaitOnButton::WaitOnButton(const char* threadName)
: _thread(zpp_lib::PreemptableThreadPriority::PriorityNormal, threadName),
_pressedTime(std::chrono::microseconds::zero()) {
_pushButton.fall(std::bind(&WaitOnButton::buttonPressed, this));
LOG_DBG("WaitOnButton initialized");
}
zpp_lib::ZephyrResult WaitOnButton::start() {
auto res = _thread.start(std::bind(&WaitOnButton::waitForButtonEvent, this));
if (!res) {
LOG_ERR("Failed to start thread: %d", (int)res.error());
return res;
}
LOG_DBG("Thread started successfully");
return res;
}
void WaitOnButton::wait_started() { _events.wait_any(kStartedEvent); }
void WaitOnButton::wait_exit() {
auto res = _thread.join();
if (!res) {
LOG_ERR("join() failed: %d", (int)res.error());
}
}
void WaitOnButton::waitForButtonEvent() {
LOG_DBG("Waiting for button press");
_events.set(kStartedEvent);
while (true) {
_events.wait_any(kPressedEvent);
std::chrono::microseconds time = zpp_lib::Time::getUpTime();
std::chrono::microseconds latency = time - _pressedTime;
LOG_DBG("Button pressed with response time: %lld usecs", latency.count());
LOG_DBG("Waiting for button press");
}
}
void WaitOnButton::buttonPressed() {
_pressedTime = zpp_lib::Time::getUpTime();
_events.set(kPressedEvent);
}
} // namespace multi_tasking
In the WaitOnButton class, we create and start an additional thread in the
WaitOnButton::start() method. This thread then executes the
WaitOnButton::waitForButtonEvent() method, which waits in an infinite loop for
a specific event to occur. This event is triggered by a button press (Button 1)
in the WaitOnButton::buttonPressed() method.
To experiment the WaitOnButton and the following multi-tasking examples, create a new “multi_tasking” application and use the code below. Ensure that the application configuration includes all necessary configuration parameters.
main.cpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file main.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Main function of the Multi-Tasking program
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
// zephyr
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
// zpp_lib
#include "zpp_include/utils.hpp"
#include "zpp_include/this_thread.hpp"
// local
#include "wait_on_button.hpp"
LOG_MODULE_REGISTER(main, CONFIG_APP_LOG_LEVEL);
int main(void) {
using namespace std::literals;
LOG_DBG("Multi-tasking program started");
// log thread statistics
zpp_lib::Utils::logThreadsSummary();
// check which button is pressed
zpp_lib::InterruptIn<zpp_lib::PinName::BUTTON1> button1;
if (button1.read() == zpp_lib::kPolarityPressed) {
LOG_DBG("Starting WaitOnButton demo");
// create the WaitOnButton instance and start it
multi_tasking::WaitOnButton waitOnButton("ButtonThread");
auto res = waitOnButton.start();
if (!res) {
LOG_ERR("Cannot start waitOnButton: %d", static_cast<int>(res.error()));
return -1;
}
// wait that the WaitOnButton thread started
LOG_DBG("Calling wait_started()");
waitOnButton.wait_started();
LOG_DBG("wait_started() unblocked");
// log thread statistics
zpp_lib::Utils::logThreadsSummary();
// wait for the thread to exit (will not because of infinite loop in WaitOnButton)
// waitOnButton.wait_exit();
// or do busy waiting
while (true) {
}
}
return 0;
}
In this example, we can observe the following:
- The use of the
WaitOnButtoninstance in the main function is self-documented. Make sure that you understand it in detail. - After starting the
WaitOnButtoninstance, the main thread waits for the thread to exit. This will never happen, so the main thread will wait forever in thewaitOnButton.wait_exit()method. Recall that the main thread must never exit themain()function for a Zephyr RTOS program to behave properly. - Each time the button is pressed, the
kPressedEventevent is set on the_eventsinstance in theWaitOnButton::buttonPressed()method. When this event is set, the “ButtonThread” thread transitions move from theWaitingto theReadystate. ThekPressedEventevent is reset on the_eventsinstance and the “ButtonThread” thread starts waiting again. You can observe this behavior by pressing the button multiple times.
To better understand the program, compile and run it. When you start the program, you should see the following in the console:
- Before the
WaitOnButtoninstance is created and started, the application runs the three “main”, “idle” and “logging” threads, as illustrated above. Note that if the “CONFIG_LOG_IMMEDIATE` option is set, then the “logging” thread is not created. - After the
WaitOnButtoninstance is started, the application creates and runs an additional thread named “ButtonThread”. - While the program is running, the “ButtonThread” thread will transition from the “Waiting” state to the “Ready” state each time the button is pressed. Then, it will immediately return to the “Waiting” state after printing its message on the console.
Note that it is possible to wait for multiple events at once, as documented in
the Events API.
This is a very useful mechanism in some situations where a task must be executed
when multiple events arise. It is also possible to wait for a specific event as
demonstrated in the WaitOnButton class.
Exercice Multi-tasking under Zephyr OS/1
It is also useful to measure the interrupt latency time. The
WaitOnButton class implements this mechanism by registering the press
time in the ISR method, and by computing the time interval until the
waitForButtonEvent thread is signaled. With this mechanism, we can
measure the interrupt latency.
You may notice that the interrupt latency varies depending on some multi-tasking behaviors. Test the following scenarios:
- The main thread is in a
Waitingstate, waiting for theWaitOnButtonthread to terminate using theThread::join()method. This is the default behavior as documented above. Try to change the priority of theWaitOnButtonthread. - Replace the call to
waitOnButton.wait_exit()in themainfunction with a busy infinite waitwhile (true) {}. - Using the busy wait, modify the priority of the
WaitOnButtonthread toPriorityAboveNormal(in the constructor) - Using the busy wait, modify the priority of the
WaitOnButtonthread toPriorityBelowNormal(in the constructor).
Report the interrupt latency times for each scenario and explain the possible reasons for the observed values.
Shared Resources and Mutual Exclusion
Multiple threads can run concurrently on a uniprocessor with interleaved or time shared execution. On multiple processor systems, multiple threads can also run simultaneously. In both cases, resource management like access to shared memory/variables by multiple threads is an issue. A multitasking or multithreaded program must make sure that resources are accessed properly, meaning in the correct sequence as if the program was executed by a single thread. In other words, in some cases, the program must enforce a mutually exclusive access to the shared resource to only one thread at a time. For this purpose, most _RTOS_es provide a so-called mutex mechanism that can be implemented both at the hardware or software level.
We first illustrate this problem with an example that simulates a clock keeping track of the current time. For updating the clock, a ticker with 1s interrupt interval is used. The structure for defining the time and the ISR for updating it are given below:
clock.hpp
// Copyright 2022 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file clock.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Clock class used for demonstrating race condition/critical section
* @date 2022-09-01
* @version 0.1.0
***************************************************************************/
#pragma once
#include "mbed.h"
namespace multi_tasking {
class Clock {
public:
struct DateTimeType {
uint32_t day;
uint32_t hour;
uint32_t minute;
uint32_t second;
};
Clock() = default;
void start() {
// start a ticker thread for dispatching events that are queued in the tickerUpdate() method
_tickerThread.start(callback(&_tickerQueue, &EventQueue::dispatch_forever));
// call the tickerUpdate() method every second, for queueing an event to be dispatched by the ticker thread
_ticker.attach(callback(this, &Clock::tickerUpdate), clockUpdateTimeout);
// schedule an event every second for displaying the time on the console
_clockDisplayQueue.call_every(clockDisplayTimeout,
callback(this, &Clock::getAndPrintDateTime));
// dispatch events from the thread calling the start() method (main thread)
_clockDisplayQueue.dispatch_forever();
}
private:
void getAndPrintDateTime() {
DateTimeType dt = {0};
dt.day = _currentTime.day;
dt.hour = _currentTime.hour;
dt.minute = _currentTime.minute;
dt.second = _currentTime.second;
printf("Day %u Hour %u min %u sec %u\n", dt.day, dt.hour, dt.minute, dt.second);
}
void tickerUpdate() {
_tickerQueue.call(callback(this, &Clock::updateCurrentTime));
}
void updateCurrentTime() {
_currentTime.second += std::chrono::duration_cast<std::chrono::seconds>(
clockUpdateTimeout).count();
if (_currentTime.second > 59) {
_currentTime.second = 0;
_currentTime.minute++;
if (_currentTime.minute > 59) {
_currentTime.minute = 0;
_currentTime.hour++;
if (_currentTime.hour > 23) {
_currentTime.hour = 0;
_currentTime.day++;
}
}
}
}
EventQueue _clockDisplayQueue;
Ticker _ticker;
EventQueue _tickerQueue;
Thread _tickerThread;
DateTimeType _currentTime{ .day = 0, .hour = 10, .minute = 59, .second = 59};
static constexpr std::chrono::milliseconds clockUpdateTimeout = 1000ms;
static constexpr std::chrono::milliseconds clockDisplayTimeout = 1000ms;
};
} // namespace multi_tasking
It is important to point out that the update of the current time (method
updateCurrentTime()) is not executed within the ISR but it is rather
deferred to the _tickerQueue event queue - so it is executed by the
_tickerThread thread. The main reason is that blocking behaviors are forbidden
in ISRs - so acquiring a mutex in an ISR is not possible. Acquiring the mutex in
the updateCurrentTime() method is not currently done, but we will show below
that it is required.
The main program used for demonstrating the use of the Clock class is shown below:
main_clock.cpp
// Copyright 2022 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file main.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief main function for demonstrating Clock
* @date 2022-09-01
* @version 0.1.0
***************************************************************************/
#include "clock.hpp"
#include "mbed.h"
#include "mbed_trace.h"
#if MBED_CONF_MBED_TRACE_ENABLE
#undef TRACE_GROUP
#define TRACE_GROUP "main"
#endif // MBED_CONF_MBED_TRACE_ENABLE
int main() {
// use trace library for console output
mbed_trace_init();
tr_debug("Clock update program started");
// create and start a clock
multi_tasking::Clock clock;
clock.start();
return 0;
}
If you execute the program, you should get the following output:
Clock times
Clock update program started
Day 0 Hour 10 min 59 sec 59
Day 0 Hour 11 min 0 sec 0
Day 0 Hour 11 min 0 sec 1
Day 0 Hour 11 min 0 sec 2
Day 0 Hour 11 min 0 sec 3
Day 0 Hour 11 min 0 sec 5
Day 0 Hour 11 min 0 sec 6
...
So everything looks fine and it seems that the above program will always execute correctly.
There is however a race condition issue in this program: the problem is
that an interrupt at the wrong time may lead to half-updated values in the
getAndPrintDateTime() method. The problem can be detailed as follows:
- Say the current time is
day: 0, hour: 10, minute: 59, second: 59or{0, 10, 59, 59}. - The first call to
getAndPrintDateTime()is executed from themain()function (as an event in the queue). The first two instructions of the function are executed and thus{ 0, 10 }are copied todt.day/hour. - A timer interrupt occurs, which updates the current time to
{0, 11, 0, 0}. - Later, the
getAndPrintDateTime()function resumes executing and copies the remaining current_time fields todt, thusdt.minute/second = {0, 0}. dtcontains the values{0, 10, 0, 0}and the program believes that the time just jumped backwards one hour !
Although the failure case described above can happen, it is unlikely to happen.
For making it easily happen, it is enough to have our getAndPrintDateTime()
method to go to sleep for \(1\,\mathsf{s}\) after the execution of the instruction
updating the hour. In this case, the ISR will for sure execute before the
execution of getAndPrintDateTime() resumes and the failure will happen. If you
modify the getAndPrintDateTime() function as follows:
clock_with_wait.hpp
...
void getAndPrintDateTime()
{
DateTimeType dt = {0};
dt.day = _currentTime.day;
dt.hour = _currentTime.hour;
static constexpr std::chrono::microseconds waitTime = 1s;
wait_us(waitTime.count());
dt.minute = _currentTime.minute;
dt.second = _currentTime.second;
printf("Day %d Hour %d min %d sec %d\n", dt.day, dt.hour, dt.minute, dt.second);
}
...
you should observe an output on the console similar to the one below. This shows a jump in the clock time:
Clock times
Clock update program started
Day 0 Hour 10 min 0 sec 0
Day 0 Hour 11 min 0 sec 2
Day 0 Hour 11 min 0 sec 3
Day 0 Hour 11 min 0 sec 4
Day 0 Hour 11 min 0 sec 5
Day 0 Hour 11 min 0 sec 6
Day 0 Hour 11 min 0 sec 7
The output above exhibits a typical race condition problem. Preemption enables
the ISR to interrupt the execution of another code and to possibly overwrite
data. To resolve this problem, one must ensure atomic or indivisible access to
the object, in our case the _currentTime object. In this case, and for
uniprocessor systems, one solution would be to disable or turn off interrupts at
the beginning of the getAndPrintDateTime() function and turn it on later. This
solution solves the problem but it may lead to missing interrupt events - in
this particular case, the current displayed time becomes incorrect.
The solution that disables interrupts during the atomic access is :
clock_with_irq_disabled.hpp
void getAndPrintDateTime() {
...
uint32_t m = __get_PRIMASK();
__disable_irq();
dt.day = _currentTime.day;
dt.hour = _currentTime.hour;
dt.minute = _currentTime.minute;
dt.second = _currentTime.second;
__set_PRIMASK(m);
...
}
In the code above, since we know that an ISR can write to our shared data
object, we disable the interrupt, after saving the current interrupt masking
state. We then restore the masking state at the end of the function. The same
code needs to be inserted in the updateCurrentTime() method for protecting
access to _currentTime. Again, although this solves the problem, disabling
interrupts should be avoided since it may affect other processing requests.
Another way of protecting the critical section is with the use of mutual exclusion objects or mutex. The goal of a mutex is to ensure that only one thread is executing in a critical section at the same time. Mutex can thus synchronize the execution of threads and protect access to a shared resource as illustrated below:
Exercice Multi-tasking under Zephyr OS/2
Remove the code for disabling interrupts and modify the Clock
class for implementing a thread safe class using a Mutex instance.
Note that using a Mutex in our example is possible only because the
execution of updateCurrentTime() is deferred to the _tickerThread thread.
Indeed, Mutex objects cannot be used from ISR context,
since acquiring a mutex is a blocking operation.
Mutex not allowed from ISR context
For experiencing the unallowed use of the Mutex mechanism in an ISR, you may modify the callback used by the ticker as follows:
...
_ticker.attach(callback(this, &Clock::updateCurrentTime), clockUpdateTimeout);
...
With this change, the updateCurrentTime() method becomes the ISR of the ticker
and is thus executed from ISR context. In this case, your program should halt
with a message stating that the use of a Mutex is not allowed from ISR context.
Using the ConditionVariable API for waiting or signaling state changes
It is worth noting that Mbed OS provides a mechanism called
ConditionVariable for
safely waiting for or signaling state changes. When protecting shared resources
with a mutex and then releasing that mutex to wait for a change of that data, a
condition variable provides a safe solution by handling the wait for a state
change, along with releasing and acquiring the mutex automatically during this
waiting period. However, unlike Events, ConditionVariable does not let
you wait on multiple events at the same time.
Deadlock
Deadlocks can arise in situations where two or more threads wait for each other to complete the task. A deadlock is thus an infinite wait and it occurs when a thread enters a wait state because a shared resource that was requested is being held by another waiting thread, which in turn is waiting for another resource held by another waiting thread.
A number of solutions have been developed for analyzing and preventing deadlock situations. For analyzing situations where deadlocks may arise, Coffman proved that deadlock situation on a resource can arise if and only if all of the following conditions illustrated in the figure below hold simultaneously:
- Mutex: exclusive resource, non-shareable (Resource A and B in the figure below)
- Resource holding: request additional resources while holding one (Requesting resource B while holding Resource A, Resource B held by another process)
- No preemption: resource can not be de-allocated or forcibly removed (Resource B cannot be deallocated)
- Circular wait: circular dependency or a closed chain of dependency (A vs B, Process 1 vs Process 2).
Deadlock prevention works by preventing one of the four Coffman conditions from occurring:
- Removing the mutual exclusion condition means that no process will have exclusive access to a resource (called non-blocking synchronization algorithms).
- The hold and wait or resource holding conditions are changed by making processes ask for all the resources they will need before starting up (difficult to implement).
- Allowing the preemption condition will cause difficulty and the processing outcome may be inconsistent or thrashing may occur.
- The final condition is the circular wait condition. Approaches that avoid circular waits include disabling interrupts during critical sections.
A very simple example for demonstrating a deadlock condition is shown below:
deadlock.hpp
// Copyright 2022 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file deadlock.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Deadlock class used for demonstrating deadlock
* @date 2022-09-01
* @version 0.1.0
***************************************************************************/
#pragma once
#include "mbed.h"
#include "mbed_trace.h"
#if MBED_CONF_MBED_TRACE_ENABLE
#define TRACE_GROUP "Deadlock"
#endif // MBED_CONF_MBED_TRACE_ENABLE
namespace multi_tasking {
class Deadlock {
public:
Deadlock(int index, const char *threadName) :
_index(index),
_thread(osPriorityNormal, OS_STACK_SIZE, nullptr, threadName) {
}
void start() {
osStatus status = _thread.start(callback(this, &Deadlock::execute));
tr_debug("Thread %s started with status %d", _thread.get_name(), status);
}
void wait() {
_thread.join();
}
private:
void execute() {
// enter the first critical section
_mutex[_index].lock();
tr_debug("Thread %d entered critical section %d", _index, _index);
// perform some operations
wait_us(kProcessingWaitTime.count());
tr_debug("Thread %d processing in mutex %d done", _index, _index);
// enter the second critical section
int secondIndex = (_index + 1) % kNbrOfMutexes;
tr_debug("Thread %d trying to enter critical section %d", _index, secondIndex);
_mutex[secondIndex].lock();
tr_debug("Thread %d entered critical section %d", _index, secondIndex);
// perform some operations
wait_us(kProcessingWaitTime.count());
tr_debug("Thread %d processing in mutex %d and %d done",
_index, _index, secondIndex);
// exit the second critical section
_mutex[secondIndex].unlock();
// perform some operations
wait_us(kProcessingWaitTime.count());
tr_debug("Thread %d processing in mutex %d done",
_index, _index);
// exit the first critical section
_mutex[_index].unlock();
}
// time that the threads should spend processing (e.g. wait in our case)
static constexpr std::chrono::microseconds kProcessingWaitTime = 1000000us;
static constexpr int kNbrOfMutexes = 2;
int _index;
Thread _thread;
// the mutex must be declared as static for being a class instance
static Mutex _mutex[kNbrOfMutexes];
};
} // namespace multi_tasking
main_deadlock.cpp
// Copyright 2022 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file main.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief main function for demonstrating Deadlock
* @date 2022-09-01
* @version 0.1.0
***************************************************************************/
#include "deadlock.hpp"
#include "mbed.h"
#include "mbed_trace.h"
#if MBED_CONF_MBED_TRACE_ENABLE
#undef TRACE_GROUP
#define TRACE_GROUP "main"
#endif // MBED_CONF_MBED_TRACE_ENABLE
// declare static variables
Mutex multi_tasking::Deadlock::_mutex[kNbrOfMutexes];
int main() {
// use trace library for console output
mbed_trace_init();
tr_debug("Deadlock program started");
// create a first deadlock instance
multi_tasking::Deadlock deadlock0(0, "Thread0");
deadlock0.start();
// create a second deadlock instance
multi_tasking::Deadlock deadlock1(1, "Thread1");
deadlock1.start();
// wait for both threads to terminate (will not because of deadlock)
deadlock0.wait();
deadlock1.wait();
return 0;
}
In this example, we use two Mutex instances for synchronizing the execution of
two threads. The program implements the circular wait as described in the
conditions and in the figure above. The output of the program is shown below. As
you can observe Thread 0 is blocked waiting for mutex 1 (owned by Thread 1) and
Thread 1 is blocked waiting for mutex 0 (owned by Thread 0), since no thread can
enter the second critical section.
Deadlock program
[DBG ][main]: Deadlock program started
[DBG ][Deadlock]: Thread Thread0 started with status 0
[DBG ][Deadlock]: Thread Thread1 started with status 0
[DBG ][Deadlock]: Thread 0 entered critical section 0
[DBG ][Deadlock]: Thread 1 entered critical section 1
[DBG ][Deadlock]: Thread 0 processing in mutex 0 done
[DBG ][Deadlock]: Thread 0 trying to enter critical section 1
[DBG ][Deadlock]: Thread 1 processing in mutex 1 done
[DBG ][Deadlock]: Thread 1 trying to enter critical section 0
Shared Data and Semaphore
Is a Mutex a Binary Semaphore?
Like a mutex, a semaphore can manage and control access to shared resources. Unlike a mutex, it does not have the concept of an owner (a mutex is owned by the thread that entered the critical section). On the contrary, a semaphore defines the maximum number of threads that are allowed to use a particular resource at the same time. A mutex is a locking mechanism while a semaphore is a signaling mechanism.
It is important to mention that mutex and semaphore are different concepts and that it is incorrect to think of a mutex as a binary semaphore. As explained in Mutexes and Semaphores Demystified, a mutex is meant to be taken and released, always in that order, by each task that uses the shared resource. By contrast, tasks that use semaphores either signal or wait - not both. In such, they are often related to producer-consumer problems. Also, mutex objects cannot be used within an ISR, while semaphores can be used to signal from ISR context to another thread - signaling is not a blocking mechanism. This makes semaphores a powerful mechanism for signaling from ISR context.
Semaphore for Access to Shared Resources
For illustrating the use of semaphores, we demonstrate a typical producer-consumer problem. The problem can be solved using different Mbed OS concurrency primitives and we will also evaluate their advantages and disadvantages of each approach.
- The problem is stated as follows (with references to methods implemented in
the
Buffer,Consumer, andProducerclasses (given below): - The scenario is executed by two separate tasks/threads: the producer thread
implemented in the
Producerclass (_producerThread) and the consumer thread implemented in theConsumerclass (_consumerThread). - Both threads share a FILO buffer implemented in the
Bufferclass (_buffer), with a single index for accessing the buffer (_index). - One producer will repeatedly produce an integer ranging from
0to19at a random rate (Producer::produce()method). It will then append the produced value to the FILO buffer (Buffer::append()method). - One consumer will repeatedly consume an integer from the FILO buffer
(
Buffer::extract()method) and consume it (Consumer::consume()method). - The FILO buffer has no mutual exclusion, under- or overflow protection. This means that the index for accessing it can be negative or exceed the buffer size. It can also be accessed concurrently by the consumer and producer threads. In this example, the size of the buffer was arbitrarily chosen and does not reflect any required value.
- For simulating computing time required for consuming or producing data,
wait_us()calls are added in theConsumer::consume()andProducer::produce()methods. The same applies for simulating the time required for appending and extracting data to/from the buffer (in theBuffer::append()andBuffer::extract()methods). - The green led is used for making append to the buffer visible, while the yellow led is used for making extract from the buffer visible. Both leds are on when one or the other event happens.
The basic implementation of the Buffer class is given below:
buffer.hpp
// Copyright 2022 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file buffer.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Buffer class used for demonstrating consumer/producer
* @date 2022-09-01
* @version 0.1.0
***************************************************************************/
#pragma once
#include "mbed.h"
namespace multi_tasking {
#if defined(TARGET_DISCO_H747I)
#define GREEN_LED LED1
#define BLUE_LED LED4
static constexpr uint8_t kLedOn = 0;
static constexpr uint8_t kLedOff = 1;
#endif
class Buffer {
public:
Buffer() :
_producerLed(GREEN_LED),
_consumerLed(BLUE_LED) {
// initialize random seed
srand(time(NULL));
_producerLed = kLedOff;
_consumerLed = kLedOff;
}
void append(uint32_t datum) {
_producerLed = kLedOn;
_buffer[_index] = datum;
_index++;
wait_us(computeRandomWaitTime(kApppendWaitTime));
_producerLed = kLedOff;
}
uint32_t extract(void) {
_consumerLed = kLedOn;
_index--;
wait_us(computeRandomWaitTime(kExtractWaitTime));
int datum = _buffer[_index];
_consumerLed = kLedOff;
return datum;
}
int computeRandomWaitTime(const std::chrono::microseconds &waitTime) {
return rand() % waitTime.count() + waitTime.count();
}
int count() {
return _index;
}
private:
static constexpr uint8_t kBufferSize = 10;
static const std::chrono::microseconds kApppendWaitTime;
static const std::chrono::microseconds kExtractWaitTime;
DigitalOut _producerLed;
DigitalOut _consumerLed;
uint32_t _buffer[kBufferSize] = {0};
int _index = 0;
};
} // namespace multi_tasking
The code of the producer that uses the Buffer class is:
producer.hpp
// Copyright 2022 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file producer.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Producer class used for demonstrating producer/consumer
* @date 2022-09-01
* @version 0.1.0
***************************************************************************/
#pragma once
#include "mbed_trace.h"
#include "buffer.hpp"
#if MBED_CONF_MBED_TRACE_ENABLE
#undef TRACE_GROUP
#define TRACE_GROUP "Producer"
#endif // MBED_CONF_MBED_TRACE_ENABLE
namespace multi_tasking {
class Producer {
public:
explicit Producer(Buffer &buffer) :
_buffer(buffer),
_producerThread(osPriorityNormal, OS_STACK_SIZE, nullptr, "ProducerThread") {
// initialize random seed
srand(time(NULL));
}
void start() {
_producerThread.start(callback(this, &Producer::producerMethod));
}
void wait() {
_producerThread.join();
}
private:
int produce(void) {
wait_us(_buffer.computeRandomWaitTime(kProduceWaitTime));
// Produce a random number ranging from 0 to kMaxRandomValue - 1
return rand() % kMaxRandomValue;
}
void producerMethod() {
while (true) {
int producerDatum = produce();
_buffer.append(producerDatum);
tr_debug("Producer datum is %d (index %d)", producerDatum, _buffer.count());
}
}
private:
static const std::chrono::microseconds kProduceWaitTime;
static constexpr uint8_t kMaxRandomValue = 20;
Buffer &_buffer;
Thread _producerThread;
};
} // namespace multi_tasking
The code of the consumer that uses the Buffer class is:
consumer.hpp
// Copyright 2022 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file consumer.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Consumer class used for demonstrating producer/consumer
* @date 2022-09-01
* @version 0.1.0
***************************************************************************/
#pragma once
#include "mbed_trace.h"
#include "buffer.hpp"
#if MBED_CONF_MBED_TRACE_ENABLE
#undef TRACE_GROUP
#define TRACE_GROUP "Consumer"
#endif // MBED_CONF_MBED_TRACE_ENABLE
namespace multi_tasking {
class Consumer {
public:
explicit Consumer(Buffer &buffer) :
_buffer(buffer),
_consumerThread(osPriorityNormal, OS_STACK_SIZE, nullptr, "ConsumerThread") {
}
void start() {
_consumerThread.start(callback(this, &Consumer::consumerMethod));
}
void wait() {
_consumerThread.join();
}
private:
void consume(int datum) {
// does nothing
wait_us(_buffer.computeRandomWaitTime(kConsumeWaitTime));
}
void consumerMethod(void) {
while (true) {
int consumerDatum = _buffer.extract();
consume(consumerDatum);
tr_debug("Consumer datum is %d (index %d)", consumerDatum, _buffer.count());
}
}
int computeRandomWaitTime(const std::chrono::microseconds &waitTime) {
return rand() % waitTime.count() + waitTime.count();
}
private:
static const std::chrono::microseconds kConsumeWaitTime;
Buffer &_buffer;
Thread _consumerThread;
};
} // namespace multi_tasking
The main program that uses both the Producer and Consumer classes is:
main_producer_consumer.cpp
// Copyright 2022 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/****************************************************************************
* @file main.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief main function for demonstrating Producer/Consumer
* @date 2022-09-01
* @version 0.1.0
***************************************************************************/
#include "buffer.hpp"
#include "consumer.hpp"
#include "mbed.h"
#include "mbed_trace.h"
#include "producer.hpp"
#if MBED_CONF_MBED_TRACE_ENABLE
#undef TRACE_GROUP
#define TRACE_GROUP "main"
#endif // MBED_CONF_MBED_TRACE_ENABLE
// declare static variables
const std::chrono::microseconds multi_tasking::Buffer::kApppendWaitTime = 500000us;
const std::chrono::microseconds multi_tasking::Buffer::kExtractWaitTime = 500000us;
const std::chrono::microseconds multi_tasking::Producer::kProduceWaitTime = 500000us;
const std::chrono::microseconds multi_tasking::Consumer::kConsumeWaitTime = 500000us;
int main() {
// use trace library for console output
mbed_trace_init();
tr_debug("Consumer producer program started");
multi_tasking::Buffer buffer;
multi_tasking::Producer producer(buffer);
multi_tasking::Consumer consumer(buffer);
producer.start();
consumer.start();
// wait for threads to terminate (will not)
producer.wait();
consumer.wait();
return 0;
}
The steps for a full understanding of this program are the following:
-
Start the program with the producer running only (comment the call to
consumer.start()in themainfunction). Make sure to run the program, describe what you observe and give an explanation of your observations. In particular, answer the following questions:- Which led is blinking?
- How is the
_indexvalue of theBufferclass changing? - What happens if you run the program long enough? (Note: make the time constant small enough for producing/consuming fast enough).
- Can any buffer underflow or overflow issue happen and what are the possible consequences?
-
Do the opposite and start the program with the consumer running only. Answer the same questions.
- Then, start the program with both the producer and consumer running. Answer
the same questions again. Experiment with different time values, simulating
both cases where production is faster than consumption and where consumption
is faster than production, by making
kApppendWaitTimeorkExtractWaitTimemuch smaller than the other waiting times.
Exercice Multi-tasking under Zephyr OS/3
Answer all questions for each different scenario above.
From the experiments and observations made above, one should be able to make the following conclusions:
- The access to the buffer should be protected and one should ensure that only
one thread can execute
append()orextract()at the same time. This mechanism can be implemented with a mutex as seen in a previous section of this codelab. - There should be a mechanism for handling buffer under- and overflow. In other words, the consumer should not be able to consume an element when the buffer is empty and the producer should not be able to produce an element when the buffer is full. This control mechanism should move the threads in a waiting state until the requested operation becomes possible. This is where semaphores enter the game!
The Semaphore`` mechanism allows the producer thread to signal when an element is
added to the buffer and the consumer thread to wait for at least one element to
be present in the buffer. TheSemaphore` also implements a counting mechanism.
For implementing the changes documented above, proceed with the following steps:
-
Add a
Mutexin theBufferclass and make sure that each access to_bufferand_indexis protected.- Run the program after implementing this change and observe whether the buffer under- or overflow issues that you encountered in the previous version still arise.
- Observe the led for checking that the access to the critical sections is exclusive - this can be observed more easily only when using large constant time values.
-
Implement a mechanism for making sure that there will be at least one element in the buffer before the consumer can consume it. This can be done by using a
Semaphorewith the initial value corresponding to no element produced. It is important to place the code for signaling and waiting for the semaphore in the correct places:- The producer and consumer must either signal or wait but not both.
- The semaphore calls must be placed correctly with the respect to the mutex lock/unlock calls.
-
Modify the waiting times again and verify whether the buffer underflow and overflow issues have been solved.
- You should have observed that the underflow issue is solved - no consumption
possible when the buffer is empty. For solving the overflow issue, we should
make sure that no element can be produced when the buffer is full. This can
also be implemented with another
Semaphore`` initialized at the buffer capacity and with reversed roles - the producer waits for the semaphore while the consumer signals it. Add another semaphore in theBuffer` class and make sure that both under- and overflow problems are resolved.
Exercice Multi-tasking under Zephyr OS/4
Implement a fully functional Buffer class, based on the instructions above.
Exercice Multi-tasking under Zephyr OS/5
Once you have implemented a fully functional Buffer class, with the correct use of semaphores,
consider removing the call to Semaphore::release() or Semaphore::acquire(). Explain what would
happen in each case.
Built-in mechanism: Queue and Mail
At this point, it is worth mentioning that RTOS usually provide other
built-in mechanisms for consumer-producer related problems. With Mbed OS,
these mechanisms are the
Queue and the
Mail mechanisms. In the
next step, we will implement the solution to the consumer-producer problem with
the Queue mechanism.
As a side note, it is interesting to mention that all changes required for
making the implementation correct were made solely in the Buffer class. This
shows that encapsulation of a functionality with a well designed API facilitates
the development, debugging and maintenance of a well designed program.
Shared Data and Queue
As stated before, RTOS usually provide higher level abstractions for
implementing buffers and producer/consumer scenarios. Here, we implement the
same Buffer class as in the previous step - it provides the same API. The
class must use a Queue
data field rather than all buffer, mutex and semaphore related fields. The size
of the Queue must be the same as in the previous step.
The declaration of the Queue data field must thus be
...
Queue<int, kBufferSize> _producerConsumerQueue;
...
Exercice Multi-tasking under Zephyr OS/6
Once you replace the mentioned fields with the definition shown above, you need
to modify the append(), extract() and count() methods accordingly (e.g.
using try_put_for(), try_get_for() and count() methods from the Queue
class). Make the changes, compile and run the program. By modifying the waiting
times appropriately, you should notice that the consumer never consumes an
element when the queue is empty and that no element is produced beyond the queue
capacity.
The Mail mechanism is very similar to the Queue mechanism, with the added
functionality of being able to allocate blocks of memory for exchanging data
between a consumer and a producer. The Queue mechanism provided in Mbed OS
allows to exchange only integer or pointer values, which is sufficient in our
example. The Mail mechanism is not demonstrated in this codelab.