Synchronization¶
Locks, wake-ups, event flags and critical sections: which one to use, a compiled example of each, and what each costs.
| Class | Use it to | Blocks | Kernel object |
|---|---|---|---|
RtosMutex |
Protect shared data, with std::lock_guard |
Yes | Recursive mutex, created in the constructor |
RtosSharedMutex |
Many readers, one writer | Yes | Two mutexes |
Mutex + MutexGuard |
Lock a named mutex with a timeout, RAII | Yes | Mutex, created on the first lock |
SignalSemaphore |
Wake one task from another | Yes | Counting semaphore starting at 0, created on first use |
OsEventFlags |
Wait for any or all of up to 24 event bits | Yes | Event group, created in the constructor |
CriticalGuard |
Keep interrupts and the other core out of a few lines | No | None |
RtosMutex¶
The mutex for new code. It is recursive (the owner may lock it again), satisfies the standard Lockable
requirements, so std::lock_guard, std::unique_lock and std::scoped_lock work, and can be moved. Take() and
Give() are aliases for code written against a semaphore-style API.
#include "RtosMutex.h"
static RtosMutex config_mutex;
static int config_value = 0;
void SetConfig(int v) {
std::lock_guard<RtosMutex> lock(config_mutex); // unlocks at the end of the scope
config_value = v;
}
bool TrySetConfig(int v) {
if (!config_mutex.try_lock_for(/*timeout_ms=*/10)) {
return false;
}
config_value = v;
config_mutex.unlock();
return true;
}
| Method | Returns |
|---|---|
lock() |
True when locked (waits forever) |
try_lock() |
True when locked without waiting |
try_lock_for(ms) |
True when locked within ms |
unlock() |
Nothing |
RtosSharedMutex adds lock_shared() / unlock_shared() for readers; RtosUniqueLock<M> and
RtosSharedLock<M> are RAII locks with an optional timeout.
Mutex and MutexGuard¶
A named mutex and its RAII guard. MutexGuard takes the mutex in its
constructor, waiting up to the given time (250 ms by default), reports through the optional bool* whether it
got it, and releases it in its destructor.
#include "Mutex.h"
#include "MutexGuard.h"
static Mutex bus_mutex("Bus");
bool WriteBus() {
bool locked = false;
MutexGuard guard(bus_mutex, /*maxMsecToWait=*/50, &locked);
if (!locked) {
return false; // another task held it for 50 ms
}
// talk to the bus
return true;
}
Mutex::Lock(ms) and Mutex::Unlock() are the direct calls. The kernel mutex is created on the first lock
(EnsureInitialized()).
SignalSemaphore¶
A named semaphore that starts empty: one side calls Signal(), the other waits in WaitUntilSignalled(ms). Each
Signal() releases one wait. BaseThread uses one to receive its start signal. The name is copied into a 40-byte
buffer in the object, and the kernel semaphore is created on first use.
#include "SignalSemaphore.h"
static SignalSemaphore data_ready("DataReady");
void OnDataReady() { data_ready.Signal(); } // producer task
bool WaitForData() {
return data_ready.WaitUntilSignalled(/*msecToWait=*/200); // consumer task
}
OsEventFlags¶
An event group with a typed wait mode. Wait() blocks until the requested bits arrive (WaitMode::Any or
WaitMode::All) or the timeout passes, and writes the bits it saw to its last argument.
Wait() returns true only when the condition was met; on a timeout it returns false and the bits that had
arrived are still written out. It never clears bits on any backend: call Clear() when you have handled them, as
the example does.
#include "OsEventFlags.h"
constexpr uint32_t kAdcDone = 1u << 0;
constexpr uint32_t kEncoderDone = 1u << 1;
static OsEventFlags<> sensors("Sensors");
void AdcFinished() { sensors.Set(kAdcDone); }
void EncoderFinished() { sensors.Set(kEncoderDone); }
bool WaitForBoth() {
constexpr uint32_t kBoth = kAdcDone | kEncoderDone;
uint32_t bits = 0;
if (!sensors.Wait(kBoth, WaitMode::All, /*timeout_ms=*/5, bits)) {
return false; // timed out; bits holds what had arrived
}
sensors.Clear(kBoth); // Wait() leaves the bits set
return true;
}
On FreeRTOS an event group holds 24 usable bits (the top 8 are reserved by the kernel). The template parameter
of OsEventFlags<> is unused and kept for source compatibility.
CriticalGuard¶
Masks interrupts (and, on SMP FreeRTOS, takes the backend's spinlock so the other core waits) from construction to destruction. Keep the guarded code to a few instructions: no blocking calls, no logging.
#include "CriticalGuard.h"
static volatile uint32_t isr_counter = 0;
uint32_t ReadAndResetCounter() {
CriticalGuard guard; // interrupts masked until the end of scope
const uint32_t n = isr_counter;
isr_counter = 0;
return n;
}
Memory and timing¶
- After the kernel object exists (constructor or first use, as in the table above), no call allocates.
- Every blocking call takes a timeout in milliseconds;
UINT32_MAXwaits forever, 0 does not wait. - None of these calls is for interrupt context.
Next: Queues.