Skip to content

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_MAX waits forever, 0 does not wait.
  • None of these calls is for interrupt context.

Next: Queues.