Multi-tasking under Mbed OS
Introduction
Multi-tasking Programs
In this codelab, we will learn basic principle for creating multi-tasking application using Mbed OS, 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
- Mbed Studio 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 Mbed OS
Mbed OS runs on single-microcontroller embedded systems. In this context, a thread is an independent segment of the program that executes within the single process running on the microcontroller. Threading allows multiple tasks to run concurrently using a scheduler. It is important to point out that threads provide a lot of flexibility to the developer but come at the cost of resources for the thread itself and for the scheduler.
Your application (main function) starts execution in the main thread, but it’s not the only thread running in Mbed OS. When starting an Mbed OS application, there are three threads running system services:
- Main: The default thread that executes the application’s main function. The main thread has 4KiB of stack space by default. The application can configure it in “mbed_app.json” by defining the `MAIN_STACK_SIZE`` parameter, as follows (note that “config” is a node at the top level of the configuration parameters)
...
"config": {
"main-stack-size": {
"value": 8192
}
}
...
-
Idle: The thread that’s run by the scheduler when there’s no other activity in the system (for example, all other threads are waiting for some event). It’s used to make sure the board is not burning empty processor cycles, but is put to sleep for as long as possible.
-
Timer: The thread that handles system and user timer objects.
Using the MemoryLogger
helper class for logging thread info
A MemoryLogger
class is made available in the AdvEmbSof
library. You
may use this class in your BikeSystem
implementation for logging statistics
related to each thread running in your application.
If you add a MemoryLogger
instance as data member of your BikeComputer
class
and add a call to the getAndPrintStatistics()
method at the start of the
BikeComputer::start()
method, you may observe these three different threads,
as shown below:
Thread statistics logging
[DBG ][MemoryLogger]: Thread Info:
[DBG ][MemoryLogger]: Thread: 0
[DBG ][MemoryLogger]: Thread Id: 0x240035b0 with name main, state Running, priority 24
[DBG ][MemoryLogger]: Stack size 8192 (free bytes remaining 5816)
[DBG ][MemoryLogger]: Thread: 1
[DBG ][MemoryLogger]: Thread Id: 0x24003528 with name rtx_idle, state Ready, priority 1
[DBG ][MemoryLogger]: Stack size 896 (free bytes remaining 832)
[DBG ][MemoryLogger]: Thread: 2
[DBG ][MemoryLogger]: Thread Id: 0x2400356c with name rtx_timer, state Waiting, priority 40
[DBG ][MemoryLogger]: Stack size 768 (free bytes remaining 672)
Using EventFlags 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 to wait for a user to press for a button. For this purpose, Mbed OS provides the EventFlags API.
For demonstrating the use of this API, we create a WaitOnButton
class defined as follows:
wait_on_button.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 wait_on_button.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief WaitOnButton class used for demonstrating EventFlags
* @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 "WaitOnButton"
#endif // MBED_CONF_MBED_TRACE_ENABLE
namespace multi_tasking {
#if defined(TARGET_DISCO_H747I)
#define PUSH_BUTTON BUTTON1
#define POLARITY_PRESSED 0
#endif
class WaitOnButton {
public:
explicit WaitOnButton(const char* threadName) :
_thread(osPriorityNormal, OS_STACK_SIZE, nullptr, threadName),
_pushButton(PUSH_BUTTON)
{
_pushButton.fall(callback(this, &WaitOnButton::buttonPressed));
_timer.start();
_pressedTime = std::chrono::microseconds::zero();
}
void start() {
osStatus status = _thread.start(callback(this, &WaitOnButton::waitForButtonEvent));
tr_debug("Thread %s started with status %d", _thread.get_name(), status);
}
void wait_started() {
_eventFlags.wait_any(kStartedEventFlag);
}
void wait_exit() {
_thread.join();
}
private:
void waitForButtonEvent() {
tr_debug("Waiting for button press");
_eventFlags.set(kStartedEventFlag);
while (true) {
_eventFlags.wait_all(kPressedEventFlag);
std::chrono::microseconds time = _timer.elapsed_time();
std::chrono::microseconds latency = time - _pressedTime;
tr_debug("Button pressed with response time: %lld usecs", latency.count());
tr_debug("Waiting for button press");
}
}
void buttonPressed() {
_pressedTime = _timer.elapsed_time();
_eventFlags.set(kPressedEventFlag);
}
static constexpr uint8_t kPressedEventFlag = (1UL << 0);
static constexpr uint8_t kStartedEventFlag = (1UL << 1);
Thread _thread;
Timer _timer;
std::chrono::microseconds _pressedTime;
EventFlags _eventFlags;
InterruptIn _pushButton;
};
} // namespace multi_tasking
In the WaitOnButton
class, we create an additional thread and start it in the
WaitOnButton::start()
method. The thread executes the
WaitOnButton::waitForButtonEvent()
method that waits in an infinite loop for a
specific event to happen. The event is set upon button press in the
WaitOnButton::buttonPressed()
method.
The code of the main function is shown below:
main_button.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 WaitOnButton
* @date 2022-09-01
* @version 0.1.0
***************************************************************************/
#include "mbed.h"
#include "mbed_trace.h"
#include "wait_on_button.hpp"
#include "memory_logger.hpp"
#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("EventFlags program started\n");
// log thread statistics
advembsof::MemoryLogger memoryLogger;
memoryLogger.getAndPrintThreadStatistics();
// create the WaitOnButton instance and start it
multi_tasking::WaitOnButton waitOnButton("ButtonThread");
waitOnButton.start();
// wait that the WaitOnButton thread started
waitOnButton.wait_started();
// log thread statistics
memoryLogger.getAndPrintThreadStatistics();
// wait for the thread to exit (will not because of infinite loop)
waitOnButton.wait_exit();
// or do busy waiting
// while (true) {
//}
return 0;
}
In this example, we may note the following:
- The use of the
WaitOnButton
instance in the main function is (self-)documented. Make sure that you understand it in details. - After starting the
WaitOnButton
instance, the main thread then waits that the thread exited. It will not happen and the main thread will wait forever in thewaitOnButton.wait_exit()
method. Note that it is required that the main thread never exits from themain()
function for a proper behavior of an Mbed OS program. - Each time the button is pressed, the flag
kPressedEventFlag
is set on the_eventFlags
instance in theWaitOnButton::buttonPressed()
method. When this event is set, the “ButtonThread” thread will move from theWaitingEventFlag
to theRunning
state. ThekPressedEventFlag
flag will be reset on the_eventFlags
instance and the “ButtonThread” thread will start waiting again. You may experience this behavior by pressing multiple times on the button.
For a better understanding of the program, you may compile and run it. Upon program startup, you should observe the following in the console:
- Before the
WaitOnButton
instance is created and started, the application runs the three “main”, “rtx_idle” and “rtx_timer” threads, as illustrated above. - After the
WaitOnButton
instance is started, an additional thread named “ButtonThread” is created and run by the application. - When the program is running, each time the button is pressed, the “ButtonThread” thread
will move from the
WaitingEventFlag
state to theRunning
state and enter theWaitingEventFlag
state immediately after printing its message on the console. This behavior is depicted in the diagram below (yellow oval):
Note that it is possible to wait for multiple events at once, as documented in
the EventFlags 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 Mbed OS/1
It is also useful to measure the interrupt latency time in this example. The
WaitOnButton
class implements such a 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 may then
measure the interrupt latency.
You may observe that the interrupt latency varies depending on some multi-tasking behaviors. Test the following scenarios:
- The main thread is in a
Waiting
state, waiting for theWaitOnButton
thread to terminate using theThread::join
method. This is the default behavior as documented above. - Replace the call to
Thread::join
in themain
function with a busy infinite waitwhile (true) {}
. - Using the busy wait, modify the priority of the
WaitOnButton
thread toosPriorityAboveNormal
(in the constructor) - Using the busy wait, modify the priority of the
WaitOnButton
thread toosPriorityBelowNormal
(in the constructor).
Report the interrupt latency times for each scenario and explain what are possible reasons for the observed values.
Solution
For the different scenarios:
Thread::join
: observed latency times should be around \(10\,\mathsf{us}\), with a very low jitter. The reason is that the main thread is in waiting state and it will never be running. The ISR is thus served very fast and with very little jitter.- Busy wait (button thread with normal priority): the observed latency time should be around \(1\,\mathsf{ms}\) with a larger jitter. The reason is that the main thread is always running and context switching to the routine serving the deferred ISR happens through round-robin.
- Busy wait (button thread with higher priority): the routine serving the deferred ISR prempts the main thread and the observed latency time should be around \(30\,\mathsf{us}\), with very small jitter.
- Busy wait (button thread with lower priority): the routine serving the deferred _ISR is never executed, because the main thread is running with higher priority, without any interruption (premption).
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: 59
or{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}
. dt
contains 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 Mbed 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.
Solution
The following steps are required:
- Remove the code for disabling interrupts in all methods.
- Add a
Mutex
as a data member of theClock
class. - Add the code for protecting access to
_currentTime
in thegetAndPrintDateTime()
andupdateCurrentTime()
methods.
The solution is given here:
clock_with_mutex.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_with_mutex.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 <chrono>
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};
_currentTimeMutex.lock();
dt.day = _currentTime.day;
dt.hour = _currentTime.hour;
dt.minute = _currentTime.minute;
dt.second = _currentTime.second;
_currentTimeMutex.unlock();
printf("Day %d Hour %d min %d sec %d\n", dt.day, dt.hour, dt.minute, dt.second);
}
void tickerUpdate() {
_tickerQueue.call(callback(this, &Clock::updateCurrentTime));
}
void updateCurrentTime() {
_currentTimeMutex.lock();
_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++;
}
}
}
_currentTimeMutex.unlock();
}
EventQueue _clockDisplayQueue;
Ticker _ticker;
EventQueue _tickerQueue;
Thread _tickerThread;
DateTimeType _currentTime{ .day = 0, .hour = 10, .minute = 59, .second = 59};
Mutex _currentTimeMutex;
static constexpr std::chrono::milliseconds clockUpdateTimeout = 1000ms;
static constexpr std::chrono::milliseconds clockDisplayTimeout = 1000ms;
};
} // namespace multi_tasking
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 EventFlags
, 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
, andProducer
classes (given below): - The scenario is executed by two separate tasks/threads: the producer thread
implemented in the
Producer
class (_producerThread
) and the consumer thread implemented in theConsumer
class (_consumerThread
). - Both threads share a FILO buffer implemented in the
Buffer
class (_buffer
), with a single index for accessing the buffer (_index
). - One producer will repeatedly produce an integer ranging from
0
to19
at 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 themain
function). 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
_index
value of theBuffer
class 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
kApppendWaitTime
orkExtractWaitTime
much smaller than the other waiting times.
Exercice Multi-tasking under Mbed OS/3
Answer all questions for each different scenario above.
Solution
Answers for each scenario
-
Producer only:
- The producer led only.
_index
is increasing only (by1
at eachappend
). Since the memory gets corrupted,_index
may get corrupted values once the buffer is full and data continues to be appended.- The program will eventually crash (it will produce data in the buffer array beyond its range).
- Buffer overflow, crash when memory gets corrupted.
-
Consumer only:
- The consumer led only.
_index
is decreasing only (by1
at eachappend
). The_index
value is not corrupted.- The program will eventually crash, but it will take much longer since no memory gets corrupted (the consumer only reads data).
- Buffer underflow, the program may crash when it starts reading from an invalid address.
-
Producer and consumer, producer faster:
- Both leds.
_index
is both decreasing and increasing, but it is increasing faster. At some point, it may get corrupted.- The program will eventually crash (it will produce data in the buffer array beyond its range).
- Buffer overflow, crash when memory gets corrupted.
-
Producer and consumer, consumer faster:
- Both leds.
_index
is both decreasing and increasing, but it is decreasing faster.- The program will eventually crash, but it will take much longer since no memory gets corrupted (the consumer only reads data).
- Buffer underflow, the program may crash when it starts reading from an invalid address.
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. The
Semaphore` also implements a counting mechanism.
For implementing the changes documented above, proceed with the following steps:
-
Add a
Mutex
in theBuffer
class and make sure that each access to_buffer
and_index
is 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
Semaphore
with 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 the
Buffer` class and make sure that both under- and overflow problems are resolved.
Exercice Multi-tasking under Mbed OS/4
Implement a fully functional Buffer
class, based on the instructions above.
Solution
// 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) {
// make sure that we can produce without overflow
_inSemaphore.acquire();
// lock buffer
_producerConsumerMutex.lock();
_producerLed = kLedOn;
_buffer[_index] = datum;
_index++;
// unlock buffer
_producerConsumerMutex.unlock();
// tell that one element is available for consumer
_outSemaphore.release();
wait_us(computeRandomWaitTime(kApppendWaitTime));
_producerLed = kLedOff;
}
uint32_t extract(void) {
// make sure that we can consume without underflow
_outSemaphore.acquire();
// lock buffer
_producerConsumerMutex.lock();
_consumerLed = kLedOn;
_index--;
wait_us(computeRandomWaitTime(kExtractWaitTime));
int datum = _buffer[_index];
// unlock buffer
_producerConsumerMutex.unlock();
// tell that one element is available for producer
_inSemaphore.release();
_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;
Mutex _producerConsumerMutex;
Semaphore _outSemaphore {0};
Semaphore _inSemaphore {kBufferSize - 1};
uint32_t _buffer[kBufferSize] = {0};
int _index = 0;
};
} // namespace multi_tasking
Exercice Multi-tasking under Mbed 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.
Solution
There are two calls to Semaphore::release()
or Semaphore::acquire()
:
- remove call to
Semaphore::acquire()
inBuffer::append()
: this call make sure that there is no overflow at production. Removing the call allows overflow. - remove call to
Semaphore::release()
inBuffer::append()
: this call make sure that the production of an element is signaled. Removing the call will prevent consumption of elements once the buffer is empty. - remove call to
Semaphore::acquire()
inBuffer::extract()
: this call make sure that there is no underflow at consumption. Removing the call allows underflow. - remove call to
Semaphore::release()
inBuffer::extract()
: this call make sure that the consumption of an element is signaled. Removing the call will prevent production of elements once the buffer is full.
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 Mbed 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.
Solution
The Buffer
class rewritten for using Queue
is given below:
// 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_queue.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 LED2
#define YELLOW_LED LED1
#define LED_ON 1
#define LED_OFF 0
#endif
class Buffer {
public:
Buffer() :
_producerLed(GREEN_LED),
_consumerLed(YELLOW_LED) {
// initialize random seed
srand(time(NULL));
_producerLed = LED_OFF;
_consumerLed = LED_OFF;
}
void append(uint32_t datum) {
_producerLed = LED_ON;
uint32_t datum_uint32t = datum;
_producerConsumerQueue.try_put_for(Kernel::wait_for_u32_forever, (uint32_t*) datum);
wait_us(computeRandomWaitTime(kApppendWaitTime));
_producerLed = LED_OFF;
}
uint32_t extract(void) {
_consumerLed = LED_ON;
wait_us(computeRandomWaitTime(kExtractWaitTime));
uint32_t datum = 0;
_producerConsumerQueue.try_get_for(Kernel::wait_for_u32_forever, (uint32_t**) &datum);
_consumerLed = LED_OFF;
return datum;
}
int computeRandomWaitTime(const std::chrono::microseconds &waitTime) {
return rand() % waitTime.count() + waitTime.count();
}
int count() {
return _producerConsumerQueue.count() - 1;
}
private:
static constexpr uint8_t kBufferSize = 10;
static const std::chrono::microseconds kApppendWaitTime;
static const std::chrono::microseconds kExtractWaitTime;
DigitalOut _producerLed;
DigitalOut _consumerLed;
Queue<uint32_t, kBufferSize> _producerConsumerQueue;
};
} // namespace multi_tasking
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.