pw_sync

The pw_sync module contains utilities for synchronizing between threads and/or interrupts through signaling primitives and critical section lock primitives.

Warning

This module is still under construction, the API is not yet stable.

Note

The objects in this module do not have an Init() style public API which is common in many RTOS C APIs. Instead, they rely on being able to invoke the native initialization APIs for synchronization primitives during C++ construction. In order to support global statically constructed synchronization without constexpr constructors, the user and/or backend MUST ensure that any initialization required in your environment is done prior to the creation and/or initialization of the native synchronization primitives (e.g. kernel initialization).

Critical Section Lock Primitives

The critical section lock primitives provided by this module comply with BasicLockable, Lockable, and where relevant TimedLockable C++ named requirements. This means that they are compatible with existing helpers in the STL’s <mutex> thread support library. For example std::lock_guard and std::unique_lock can be directly used.

Mutex

The Mutex is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads. It offers exclusive, non-recursive ownership semantics where priority inheritance is used to solve the classic priority-inversion problem.

The Mutex’s API is C++11 STL std::mutex like, meaning it is a BasicLockable and Lockable.

Supported on

Backend module

FreeRTOS

pw_sync_freertos

ThreadX

pw_sync_threadx

embOS

pw_sync_embos

STL

pw_sync_stl

Baremetal

Planned

Zephyr

Planned

CMSIS-RTOS API v2 & RTX5

Planned

C++

class pw::sync::Mutex
void lock()

Locks the mutex, blocking indefinitely. Failures are fatal.

Precondition: The lock isn’t already held by this thread. Recursive locking is undefined behavior.

bool try_lock()

Tries to lock the mutex in a non-blocking manner. Returns true if the mutex was successfully acquired.

Precondition: The lock isn’t already held by this thread. Recursive locking is undefined behavior.

void unlock()

Unlocks the mutex. Failures are fatal.

Precondition: The mutex is held by this thread.

Safe to use in context

Thread

Interrupt

NMI

Mutex::Mutex

Mutex::~Mutex

void Mutex::lock

bool Mutex::try_lock

void Mutex::unlock

Examples in C++
#include "pw_sync/mutex.h"

pw::sync::Mutex mutex;

void ThreadSafeCriticalSection() {
  mutex.lock();
  NotThreadSafeCriticalSection();
  mutex.unlock();
}

Alternatively you can use C++’s RAII helpers to ensure you always unlock.

#include <mutex>

#include "pw_sync/mutex.h"

pw::sync::Mutex mutex;

void ThreadSafeCriticalSection() {
  std::lock_guard lock(mutex);
  NotThreadSafeCriticalSection();
}

C

The Mutex must be created in C++, however it can be passed into C using the pw_sync_Mutex opaque struct alias.

void pw_sync_Mutex_Lock(pw_sync_Mutex *mutex)

Invokes the Mutex::lock member function on the given mutex.

bool pw_sync_Mutex_TryLock(pw_sync_Mutex *mutex)

Invokes the Mutex::try_lock member function on the given mutex.

void pw_sync_Mutex_Unlock(pw_sync_Mutex *mutex)

Invokes the Mutex::unlock member function on the given mutex.

Safe to use in context

Thread

Interrupt

NMI

void pw_sync_Mutex_Lock

bool pw_sync_Mutex_TryLock

void pw_sync_Mutex_Unlock

Example in C
#include "pw_sync/mutex.h"

pw::sync::Mutex mutex;

extern pw_sync_Mutex mutex;  // This can only be created in C++.

void ThreadSafeCriticalSection(void) {
  pw_sync_Mutex_Lock(&mutex);
  NotThreadSafeCriticalSection();
  pw_sync_Mutex_Unlock(&mutex);
}

TimedMutex

The TimedMutex is an extension of the Mutex which offers timeout and deadline based semantics.

The TimedMutex’s API is C++11 STL std::timed_mutex like, meaning it is a BasicLockable, Lockable, and TimedLockable.

Note that the TimedMutex is a derived Mutex class, meaning that a TimedMutex can be used by someone who needs the basic Mutex. This is in contrast to the C++ STL’s std::timed_mutex.

Supported on

Backend module

FreeRTOS

pw_sync_freertos

ThreadX

pw_sync_threadx

embOS

pw_sync_embos

STL

pw_sync_stl

Zephyr

Planned

CMSIS-RTOS API v2 & RTX5

Planned

C++

class pw::sync::TimedMutex
void lock()

Locks the mutex, blocking indefinitely. Failures are fatal.

Precondition: The lock isn’t already held by this thread. Recursive locking is undefined behavior.

bool try_lock()

Tries to lock the mutex in a non-blocking manner. Returns true if the mutex was successfully acquired.

Precondition: The lock isn’t already held by this thread. Recursive locking is undefined behavior.

bool try_lock_for(const chrono::SystemClock::duration &timeout)

Tries to lock the mutex. Blocks until specified the timeout has elapsed or the lock is acquired, whichever comes first. Returns true if the mutex was successfully acquired.

Precondition: The lock isn’t already held by this thread. Recursive locking is undefined behavior.

bool try_lock_until(const chrono::SystemClock::time_point &deadline)

Tries to lock the mutex. Blocks until specified deadline has been reached or the lock is acquired, whichever comes first. Returns true if the mutex was successfully acquired.

Precondition: The lock isn’t already held by this thread. Recursive locking is undefined behavior.

void unlock()

Unlocks the mutex. Failures are fatal.

Precondition: The mutex is held by this thread.

Safe to use in context

Thread

Interrupt

NMI

TimedMutex::TimedMutex

TimedMutex::~TimedMutex

void TimedMutex::lock

bool TimedMutex::try_lock

bool TimedMutex::try_lock_for

bool TimedMutex::try_lock_until

void TimedMutex::unlock

Examples in C++
#include "pw_chrono/system_clock.h"
#include "pw_sync/timed_mutex.h"

pw::sync::TimedMutex mutex;

bool ThreadSafeCriticalSectionWithTimeout(
    const SystemClock::duration timeout) {
  if (!mutex.try_lock_for(timeout)) {
    return false;
  }
  NotThreadSafeCriticalSection();
  mutex.unlock();
  return true;
}

Alternatively you can use C++’s RAII helpers to ensure you always unlock.

#include <mutex>

#include "pw_chrono/system_clock.h"
#include "pw_sync/timed_mutex.h"

pw::sync::TimedMutex mutex;

bool ThreadSafeCriticalSectionWithTimeout(
    const SystemClock::duration timeout) {
  std::unique_lock lock(mutex, std::defer_lock);
  if (!lock.try_lock_for(timeout)) {
    return false;
  }
  NotThreadSafeCriticalSection();
  return true;
}

C

The TimedMutex must be created in C++, however it can be passed into C using the pw_sync_TimedMutex opaque struct alias.

void pw_sync_TimedMutex_Lock(pw_sync_TimedMutex *mutex)

Invokes the TimedMutex::lock member function on the given mutex.

bool pw_sync_TimedMutex_TryLock(pw_sync_TimedMutex *mutex)

Invokes the TimedMutex::try_lock member function on the given mutex.

bool pw_sync_TimedMutex_TryLockFor(pw_sync_TimedMutex *mutex, pw_chrono_SystemClock_Duration timeout)

Invokes the TimedMutex::try_lock_for member function on the given mutex.

bool pw_sync_TimedMutex_TryLockUntil(pw_sync_TimedMutex *mutex, pw_chrono_SystemClock_TimePoint deadline)

Invokes the TimedMutex::try_lock_until member function on the given mutex.

void pw_sync_TimedMutex_Unlock(pw_sync_TimedMutex *mutex)

Invokes the TimedMutex::unlock member function on the given mutex.

Safe to use in context

Thread

Interrupt

NMI

void pw_sync_TimedMutex_Lock

bool pw_sync_TimedMutex_TryLock

bool pw_sync_TimedMutex_TryLockFor

bool pw_sync_TimedMutex_TryLockUntil

void pw_sync_TimedMutex_Unlock

Example in C
#include "pw_chrono/system_clock.h"
#include "pw_sync/timed_mutex.h"

pw::sync::TimedMutex mutex;

extern pw_sync_TimedMutex mutex;  // This can only be created in C++.

bool ThreadSafeCriticalSectionWithTimeout(
    const pw_chrono_SystemClock_Duration timeout) {
  if (!pw_sync_TimedMutex_TryLockFor(&mutex, timeout)) {
    return false;
  }
  NotThreadSafeCriticalSection();
  pw_sync_TimedMutex_Unlock(&mutex);
  return true;
}

InterruptSpinLock

The InterruptSpinLock is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads and/or interrupts as a targeted global lock, with the exception of Non-Maskable Interrupts (NMIs). It offers exclusive, non-recursive ownership semantics where IRQs up to a backend defined level of “NMIs” will be masked to solve priority-inversion.

This InterruptSpinLock relies on built-in local interrupt masking to make it interrupt safe without requiring the caller to separately mask and unmask interrupts when using this primitive.

Unlike global interrupt locks, this also works safely and efficiently on SMP systems. On systems which are not SMP, spinning is not required but some state may still be used to detect recursion.

The InterruptSpinLock is a BasicLockable and Lockable.

Supported on

Backend module

FreeRTOS

pw_sync_freertos

ThreadX

pw_sync_threadx

embOS

pw_sync_embos

STL

pw_sync_stl

Baremetal

Planned, not ready for use

Zephyr

Planned

CMSIS-RTOS API v2 & RTX5

Planned

C++

class pw::sync::InterruptSpinLock
void lock()

Locks the spinlock, blocking indefinitely. Failures are fatal.

Precondition: Recursive locking is undefined behavior.

bool try_lock()

Tries to lock the spinlock in a non-blocking manner. Returns true if the spinlock was successfully acquired.

Precondition: Recursive locking is undefined behavior.

void unlock()

Unlocks the mutex. Failures are fatal.

Precondition: The spinlock is held by the caller.

Safe to use in context

Thread

Interrupt

NMI

InterruptSpinLock::InterruptSpinLock

InterruptSpinLock::~InterruptSpinLock

void InterruptSpinLock::lock

bool InterruptSpinLock::try_lock

void InterruptSpinLock::unlock

Examples in C++
#include "pw_sync/interrupt_spin_lock.h"

pw::sync::InterruptSpinLock interrupt_spin_lock;

void InterruptSafeCriticalSection() {
  interrupt_spin_lock.lock();
  NotThreadSafeCriticalSection();
  interrupt_spin_lock.unlock();
}

Alternatively you can use C++’s RAII helpers to ensure you always unlock.

#include <mutex>

#include "pw_sync/interrupt_spin_lock.h"

pw::sync::InterruptSpinLock interrupt_spin_lock;

void InterruptSafeCriticalSection() {
  std::lock_guard lock(interrupt_spin_lock);
  NotThreadSafeCriticalSection();
}

C

The InterruptSpinLock must be created in C++, however it can be passed into C using the pw_sync_InterruptSpinLock opaque struct alias.

void pw_sync_InterruptSpinLock_Lock(pw_sync_InterruptSpinLock *interrupt_spin_lock)

Invokes the InterruptSpinLock::lock member function on the given interrupt_spin_lock.

bool pw_sync_InterruptSpinLock_TryLock(pw_sync_InterruptSpinLock *interrupt_spin_lock)

Invokes the InterruptSpinLock::try_lock member function on the given interrupt_spin_lock.

void pw_sync_InterruptSpinLock_Unlock(pw_sync_InterruptSpinLock *interrupt_spin_lock)

Invokes the InterruptSpinLock::unlock member function on the given interrupt_spin_lock.

Safe to use in context

Thread

Interrupt

NMI

void pw_sync_InterruptSpinLock_Lock

bool pw_sync_InterruptSpinLock_TryLock

void pw_sync_InterruptSpinLock_Unlock

Example in C
#include "pw_chrono/system_clock.h"
#include "pw_sync/interrupt_spin_lock.h"

pw::sync::InterruptSpinLock interrupt_spin_lock;

extern pw_sync_InterruptSpinLock interrupt_spin_lock;  // This can only be created in C++.

void InterruptSafeCriticalSection(void) {
  pw_sync_InterruptSpinLock_Lock(&interrupt_spin_lock);
  NotThreadSafeCriticalSection();
  pw_sync_InterruptSpinLock_Unlock(&interrupt_spin_lock);
}

Thread Safety Lock Annotations

Pigweed’s critical section lock primitives support Clang’s thread safety analysis extension for C++. The analysis is completely static at compile-time. This is only supported when building with Clang. The annotations are no-ops when using different compilers.

Pigweed provides the pw_sync/lock_annotations.h header file with macro definitions to allow developers to document the locking policies of multi-threaded code. The annotations can also help program analysis tools to identify potential thread safety issues.

More information on Clang’s thread safety analysis system can be found here.

Enabling Clang’s Analysis

In order to enable the analysis, Clang requires that the -Wthread-safety compilation flag be used. In addition, if any STL components like std::lock_guard are used, the STL’s built in annotations have to be manually enabled, typically by setting the _LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS macro.

If using GN, the pw_build:clang_thread_safety_warnings config is provided to do this for you, when added to your clang toolchain definition’s default configs.

Why use lock annotations?

Lock annotations can help warn you about potential race conditions in your code when using locks: you have to remember to grab lock(s) before entering a critical section, yuou have to remember to unlock it when you leave, and you have to avoid deadlocks.

Clang’s lock annotations let you inform the compiler and anyone reading your code which variables are guarded by which locks, which locks should or cannot be held when calling which function, which order locks should be acquired in, etc.

Using Lock Annotations

When referring to locks in the arguments of the attributes, you should use variable names or more complex expressions (e.g. my_object->lock_) that evaluate to a concrete lock object whenever possible. If the lock you want to refer to is not in scope, you may use a member pointer (e.g. &MyClass::lock_) to refer to a lock in some (unknown) object.

Annotating Lock Usage
PW_GUARDED_BY(x)

Documents if a shared field or global variable needs to be protected by a lock. PW_GUARDED_BY() allows the user to specify a particular lock that should be held when accessing the annotated variable.

Although this annotation (and PW_PT_GUARDED_BY, below) cannot be applied to local variables, a local variable and its associated lock can often be combined into a small class or struct, thereby allowing the annotation.

Example:

class Foo {
  Mutex mu_;
  int p1_ PW_GUARDED_BY(mu_);
  ...
};
PW_PT_GUARDED_BY(x)

Documents if the memory location pointed to by a pointer should be guarded by a lock when dereferencing the pointer.

Example:

class Foo {
  Mutex mu_;
  int *p1_ PW_PT_GUARDED_BY(mu_);
  ...
};

Note that a pointer variable to a shared memory location could itself be a shared variable.

Example:

// `q_`, guarded by `mu1_`, points to a shared memory location that is
// guarded by `mu2_`:
int *q_ PW_GUARDED_BY(mu1_) PW_PT_GUARDED_BY(mu2_);
PW_ACQUIRED_AFTER(...)
PW_ACQUIRED_BEFORE(...)

Documents the acquisition order between locks that can be held simultaneously by a thread. For any two locks that need to be annotated to establish an acquisition order, only one of them needs the annotation. (i.e. You don’t have to annotate both locks with both PW_ACQUIRED_AFTER and PW_ACQUIRED_BEFORE.)

As with PW_GUARDED_BY, this is only applicable to locks that are shared fields or global variables.

Example:

Mutex m1_;
Mutex m2_ PW_ACQUIRED_AFTER(m1_);
PW_EXCLUSIVE_LOCKS_REQUIRED(...)
PW_SHARED_LOCKS_REQUIRED(...)

Documents a function that expects a lock to be held prior to entry. The lock is expected to be held both on entry to, and exit from, the function.

An exclusive lock allows read-write access to the guarded data member(s), and only one thread can acquire a lock exclusively at any one time. A shared lock allows read-only access, and any number of threads can acquire a shared lock concurrently.

Generally, non-const methods should be annotated with PW_EXCLUSIVE_LOCKS_REQUIRED, while const methods should be annotated with PW_SHARED_LOCKS_REQUIRED.

Example:

Mutex mu1, mu2;
int a PW_GUARDED_BY(mu1);
int b PW_GUARDED_BY(mu2);

void foo() PW_EXCLUSIVE_LOCKS_REQUIRED(mu1, mu2) { ... }
void bar() const PW_SHARED_LOCKS_REQUIRED(mu1, mu2) { ... }
PW_LOCKS_EXCLUDED(...)

Documents the locks acquired in the body of the function. These locks cannot be held when calling this function (as Pigweed’s default locks are non-reentrant).

Example:

Mutex mu;
int a PW_GUARDED_BY(mu);

void foo() PW_LOCKS_EXCLUDED(mu) {
  mu.lock();
  ...
  mu.unlock();
}
PW_LOCK_RETURNED(...)

Documents a function that returns a lock without acquiring it. For example, a public getter method that returns a pointer to a private lock should be annotated with PW_LOCK_RETURNED.

Example:

class Foo {
 public:
  Mutex* mu() PW_LOCK_RETURNED(mu) { return &mu; }

 private:
  Mutex mu;
};
PW_NO_LOCK_SAFETY_ANALYSIS()

Turns off thread safety checking within the body of a particular function. This annotation is used to mark functions that are known to be correct, but the locking behavior is more complicated than the analyzer can handle.

Annotating Lock Objects

In order of lock usage annotation to work, the lock objects themselves need to be annotated as well. In case you are providing your own lock or psuedo-lock object, you can use the macros in this section to annotate it.

As an example we’ve annotated a Lock and a RAII ScopedLocker object for you, see the macro documentation after for more details:

class PW_LOCKABLE("Lock") Lock {
 public:
  void Lock() PW_EXCLUSIVE_LOCK_FUNCTION();

  void ReaderLock() PW_SHARED_LOCK_FUNCTION();

  void Unlock() PW_UNLOCK_FUNCTION();

  void ReaderUnlock() PW_SHARED_TRYLOCK_FUNCTION();

  bool TryLock() PW_EXCLUSIVE_TRYLOCK_FUNCTION(true);

  bool ReaderTryLock() PW_SHARED_TRYLOCK_FUNCTION(true);

  void AssertHeld() PW_ASSERT_EXCLUSIVE_LOCK();

  void AssertReaderHeld() PW_ASSERT_SHARED_LOCK();
};


// Tag types for selecting a constructor.
struct adopt_lock_t {} inline constexpr adopt_lock = {};
struct defer_lock_t {} inline constexpr defer_lock = {};
struct shared_lock_t {} inline constexpr shared_lock = {};

class PW_SCOPED_LOCKABLE ScopedLocker {
  // Acquire lock, implicitly acquire *this and associate it with lock.
  ScopedLocker(Lock *lock) PW_EXCLUSIVE_LOCK_FUNCTION(lock)
      : lock_(lock), locked(true) {
    lock->Lock();
  }

  // Assume lock is held, implicitly acquire *this and associate it with lock.
  ScopedLocker(Lock *lock, adopt_lock_t) PW_EXCLUSIVE_LOCKS_REQUIRED(lock)
      : lock_(lock), locked(true) {}

  // Acquire lock in shared mode, implicitly acquire *this and associate it
  // with lock.
  ScopedLocker(Lock *lock, shared_lock_t) PW_SHARED_LOCK_FUNCTION(lock)
      : lock_(lock), locked(true) {
    lock->ReaderLock();
  }

  // Assume lock is held in shared mode, implicitly acquire *this and associate
  // it with lock.
  ScopedLocker(Lock *lock, adopt_lock_t, shared_lock_t)
      PW_SHARED_LOCKS_REQUIRED(lock) : lock_(lock), locked(true) {}

  // Assume lock is not held, implicitly acquire *this and associate it with
  // lock.
  ScopedLocker(Lock *lock, defer_lock_t) PW_LOCKS_EXCLUDED(lock)
      : lock_(lock), locked(false) {}

  // Release *this and all associated locks, if they are still held.
  // There is no warning if the scope was already unlocked before.
  ~ScopedLocker() PW_UNLOCK_FUNCTION() {
    if (locked)
      lock_->GenericUnlock();
  }

  // Acquire all associated locks exclusively.
  void Lock() PW_EXCLUSIVE_LOCK_FUNCTION() {
    lock_->Lock();
    locked = true;
  }

  // Try to acquire all associated locks exclusively.
  bool TryLock() PW_EXCLUSIVE_TRYLOCK_FUNCTION(true) {
    return locked = lock_->TryLock();
  }

  // Acquire all associated locks in shared mode.
  void ReaderLock() PW_SHARED_LOCK_FUNCTION() {
    lock_->ReaderLock();
    locked = true;
  }

  // Try to acquire all associated locks in shared mode.
  bool ReaderTryLock() PW_SHARED_TRYLOCK_FUNCTION(true) {
    return locked = lock_->ReaderTryLock();
  }

  // Release all associated locks. Warn on double unlock.
  void Unlock() PW_UNLOCK_FUNCTION() {
    lock_->Unlock();
    locked = false;
  }

  // Release all associated locks. Warn on double unlock.
  void ReaderUnlock() PW_UNLOCK_FUNCTION() {
    lock_->ReaderUnlock();
    locked = false;
  }

 private:
  Lock* lock_;
  bool locked_;
};
PW_LOCKABLE(name)

Documents if a class/type is a lockable type (such as the pw::sync::Mutex class). The name is used in the warning messages. This can also be useful on classes which have locking like semantics but aren’t actually locks.

PW_SCOPED_LOCKABLE()

Documents if a class does RAII locking. The name is used in the warning messages.

The constructor should use LOCK_FUNCTION() to specify the lock that is acquired, and the destructor should use UNLOCK_FUNCTION() with no arguments; the analysis will assume that the destructor unlocks whatever the constructor locked.

PW_EXCLUSIVE_LOCK_FUNCTION()

Documents functions that acquire a lock in the body of a function, and do not release it.

PW_SHARED_LOCK_FUNCTION()

Documents functions that acquire a shared (reader) lock in the body of a function, and do not release it.

PW_UNLOCK_FUNCTION()

Documents functions that expect a lock to be held on entry to the function, and release it in the body of the function.

PW_EXCLUSIVE_TRYLOCK_FUNCTION(try_success)
PW_SHARED_TRYLOCK_FUNCTION(try_success)

Documents functions that try to acquire a lock, and return success or failure (or a non-boolean value that can be interpreted as a boolean). The first argument should be true for functions that return true on success, or false for functions that return false on success. The second argument specifies the lock that is locked on success. If unspecified, this lock is assumed to be this.

PW_ASSERT_EXCLUSIVE_LOCK()
PW_ASSERT_SHARED_LOCK()

Documents functions that dynamically check to see if a lock is held, and fail if it is not held.

Critical Section Lock Helpers

Virtual Lock Interfaces

Virtual lock interfaces can be useful when lock selection cannot be templated.

Why use virtual locks?

Virtual locks enable depending on locks without templating implementation code on the type, while retaining flexibility with respect to the concrete lock type. Pigweed tries to avoid pushing policy on to users, and virtual locks are one way to accomplish that without templating everything.

A case when virtual locks are useful is when the concrete lock type changes at run time. For example, access to flash may be protected at run time by an internal mutex, however at crash time we may want to switch to a no-op lock. A virtual lock interface could be used here to minimize the code-size cost that would occur otherwise if the flash driver were templated.

VirtualBasicLock

The VirtualBasicLock interface meets the BasicLockable C++ named requirement. Our critical section lock primitives offer optional virtual versions, including:

  • pw::sync::VirtualMutex

  • pw::sync::VirtualTimedMutex

  • pw::sync::VirtualInterruptSpinLock

Borrowable

The Borrowable is a helper construct that enables callers to borrow an object which is guarded by a lock, enabling a containerized style of external locking.

Users who need access to the guarded object can ask to acquire a BorrowedPointer which permits access while the lock is held.

This class is compatible with locks which comply with BasicLockable, Lockable, and TimedLockable C++ named requirements.

By default the selected lock type is a pw::sync::VirtualBasicLockable. If this virtual interface is used, the templated lock parameter can be skipped.

External vs Internal locking

Before we explain why Borrowable is useful, it’s important to understand the trade-offs when deciding on using internal and/or external locking.

Internal locking is when the lock is hidden from the caller entirely and is used internally to the API. For example:

class BankAccount {
 public:
  void Deposit(int amount) {
    std::lock_guard lock(mutex_);
    balance_ += amount;
  }

  void Withdraw(int amount) {
    std::lock_guard lock(mutex_);
    balance_ -= amount;
  }

  void Balance() const {
    std::lock_guard lock(mutex_);
    return balance_;
  }

 private:
  int balance_ PW_GUARDED_BY(mutex_);
  pw::sync::Mutex mutex_;
};

Internal locking guarantees that any concurrent calls to its public member functions don’t corrupt an instance of that class. This is typically ensured by having each member function acquire a lock on the object upon entry. This way, for any instance, there can only be one member function call active at any moment, serializing the operations.

One common issue that pops up is that member functions may have to call other member functions which also require locks. This typically results in a duplication of the public API into an internal mirror where the lock is already held. This along with having to modify every thread-safe public member function may results in an increased code size.

However, with the per-method locking approach, it is not possible to perform a multi-method thread-safe transaction. For example, what if we only wanted to withdraw money if the balance was high enough? With the current API there would be a risk that money is withdrawn after we’ve checked the balance.

This is usually why external locking is used. This is when the lock is exposed to the caller and may be used externally to the public API. External locking can take may forms which may even include mixing internal and external locking. In its most simplistic form it is an external lock used along side each instance, e.g.:

class BankAccount {
 public:
  void Deposit(int amount) {
    balance_ += amount;
  }

  void Withdraw(int amount) {
    balance_ -= amount;
  }

  void Balance() const {
    return balance_;
  }

 private:
  int balance_;
};

pw::sync::Mutex bobs_account_mutex;
BankAccount bobs_account PW_GUARDED_BY(bobs_account_mutex);

The lock is acquired before the bank account is used for a transaction. In addition, we do not have to modify every public function and its trivial to call other public member functions from a public member function. However, as you can imagine instantiating and passing around the instances and their locks can become error prone.

This is why Borrowable exists.

Why use Borrowable?

Borrowable offers code-size efficient way to enable external locking that is easy and safe to use. It is effectively a container which holds references to a protected instance and its lock which provides RAII-style access.

pw::sync::Mutex bobs_account_mutex;
BankAccount bobs_account PW_GUARDED_BY(bobs_account_mutex);
pw::sync::Borrowable<BankAccount, pw::sync::Mutex> bobs_acount(
    bobs_account, bobs_account_mutex);

This construct is useful when sharing objects or data which are transactional in nature where making individual operations threadsafe is insufficient. See the section on internal vs external locking tradeoffs above.

It can also offer a code-size and stack-usage efficient way to separate timeout constraints between the acquiring of the shared object and timeouts used for the shared object’s API. For example, imagine you have an I2c bus which is used by several threads and you’d like to specify an ACK timeout of 50ms. It’d be ideal if the duration it takes to gain exclusive access to the I2c bus does not eat into the ACK timeout you’d like to use for the transaction. Borrowable can help you do exactly this if you provide access to the I2c bus through a Borrowable.

C++

template<typename GuardedType, typename Lock = pw::sync::VirtualBasicLockable>
class pw::sync::BorrowedPointer

The BorrowedPointer is an RAII handle which wraps a pointer to a borrowed object along with a held lock which is guarding the object. When destroyed, the lock is released.

This object is moveable, but not copyable.

GuardedType *operator->()

Provides access to the borrowed object’s members.

GuardedType &operator*()

Provides access to the borrowed object directly.

Warning: The member of pointer member access operator, operator->(), is recommended over this API as this is prone to leaking references. However, this is sometimes necessary.

Warning: Be careful not to leak references to the borrowed object.

template<typename GuardedType, typename Lock = pw::sync::VirtualBasicLockable>
class pw::sync::Borrowable
BorrowedPointer<GuardedType, Lock> acquire()

Blocks indefinitely until the object can be borrowed. Failures are fatal.

std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire()

Tries to borrow the object in a non-blocking manner. Returns a BorrowedPointer on success, otherwise std::nullopt (nothing).

template<class Rep, class Period>
std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire_for(std::chrono::duration<Rep, Period> timeout)

Tries to borrow the object. Blocks until the specified timeout has elapsed or the object has been borrowed, whichever comes first. Returns a BorrowedPointer on success, otherwise std::nullopt (nothing).

template<class Rep, class Period>
std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire_until(std::chrono::duration<Rep, Period> deadline)

Tries to borrow the object. Blocks until the specified deadline has been reached or the object has been borrowed, whichever comes first. Returns a BorrowedPointer on success, otherwise std::nullopt (nothing).

Example in C++
#include <chrono>

#include "pw_bytes/span.h"
#include "pw_i2c/initiator.h"
#include "pw_status/try.h"
#include "pw_status/result.h"
#include "pw_sync/borrow.h"
#include "pw_sync/mutex.h"

class ExampleI2c : public pw::i2c::Initiator;

pw::sync::VirtualMutex i2c_mutex;
ExampleI2c i2c;
pw::sync::Borrowable<ExampleI2c> borrowable_i2c(i2c, i2c_mutex);

pw::Result<ConstByteSpan> ReadI2cData(ByteSpan buffer) {
  // Block indefinitely waiting to borrow the i2c bus.
  pw::sync::BorrowedPointer<ExampleI2c> borrowed_i2c =
      borrowable_i2c.acquire();

  // Execute a sequence of transactions to get the needed data.
  PW_TRY(borrowed_i2c->WriteFor(kFirstWrite, std::chrono::milliseconds(50)));
  PW_TRY(borrowed_i2c->WriteReadFor(kSecondWrite, buffer,
                                    std::chrono::milliseconds(10)));

  // Borrowed i2c pointer is returned when the scope exits.
  return buffer;
}

Signaling Primitives

Native signaling primitives tend to vary more compared to critial section locks across different platforms. For example, although common signaling primtives like semaphores are in most if not all RTOSes and even POSIX, it was not in the STL before C++20. Likewise many C++ developers are surprised that conditional variables tend to not be natively supported on RTOSes. Although you can usually build any signaling primitive based on other native signaling primitives, this may come with non-trivial added overhead in ROM, RAM, and execution efficiency.

For this reason, Pigweed intends to provide some simpler signaling primitives which exist to solve a narrow programming need but can be implemented as efficiently as possible for the platform that it is used on.

This simpler but highly portable class of signaling primitives is intended to ensure that a portability efficiency tradeoff does not have to be made up front. Today this is class of simpler signaling primitives is limited to the pw::sync::ThreadNotification and pw::sync::TimedThreadNotification.

ThreadNotification

The ThreadNotification is a synchronization primitive that can be used to permit a SINGLE thread to block and consume a latching, saturating notification from multiple notifiers.

Note

Although only a single thread can block on a ThreadNotification at a time, many instances may be used by a single thread just like binary semaphores. This is in contrast to some native RTOS APIs, such as direct task notifications, which re-use the same state within a thread’s context.

Warning

This is a single consumer/waiter, multiple producer/notifier API! The acquire APIs must only be invoked by a single consuming thread. As a result, having multiple threads receiving notifications via the acquire API is unsupported.

This is effectively a subset of the pw::sync::BinarySemaphore API, except that only a single thread can be notified and block at a time.

The single consumer aspect of the API permits the use of a smaller and/or faster native APIs such as direct thread signaling. This should be backed by the most efficient native primitive for a target, regardless of whether that is a semaphore, event flag group, condition variable, or something else.

Generic BinarySemaphore-based Backend

This module provides a generic backend for pw::sync::ThreadNotification via pw_sync:binary_semaphore_thread_notification which uses a pw::sync::BinarySemaphore as the backing primitive. See BinarySemaphore for backend availability.

Optimized Backend

Supported on

Optimized backend module

FreeRTOS

Planned

ThreadX

Planned

embOS

Planned

STL

Not planned, use pw_sync:binary_semaphore_thread_notification

Baremetal

Planned

Zephyr

Planned

CMSIS-RTOS API v2 & RTX5

Planned

C++

class pw::sync::ThreadNotification
void acquire()

Blocks indefinitely until the thread is notified, i.e. until the notification latch can be cleared because it was set.

Clears the notification latch.

IMPORTANT: This should only be used by a single consumer thread.

bool try_acquire()

Returns whether the thread has been notified, i.e. whether the notificion latch was set and resets the latch regardless.

Clears the notification latch.

Returns true if the thread was notified, meaning the the internal latch was reset successfully.

IMPORTANT: This should only be used by a single consumer thread.

void release()

Notifies the thread in a saturating manner, setting the notification latch.

Raising the notification multiple time without it being acquired by the consuming thread is equivalent to raising the notification once to the thread. The notification is latched in case the thread was not waiting at the time.

This is IRQ and thread safe.

Safe to use in context

Thread

Interrupt

NMI

ThreadNotification::ThreadNotification

ThreadNotification::~ThreadNotification

void ThreadNotification::acquire

bool ThreadNotification::try_acquire

void ThreadNotification::release

Examples in C++
#include "pw_sync/thread_notification.h"
#include "pw_thread/thread_core.h"

class FooHandler() : public pw::thread::ThreadCore {
 // Public API invoked by other threads and/or interrupts.
 void NewFooAvailable() {
   new_foo_notification_.release();
 }

 private:
  pw::sync::ThreadNotification new_foo_notification_;

  // Thread function.
  void Run() override {
    while (true) {
      new_foo_notification_.acquire();
      HandleFoo();
    }
  }

  void HandleFoo();
}

TimedThreadNotification

The TimedThreadNotification is an extension of the ThreadNotification which offers timeout and deadline based semantics.

Warning

This is a single consumer/waiter, multiple producer/notifier API! The acquire APIs must only be invoked by a single consuming thread. As a result, having multiple threads receiving notifications via the acquire API is unsupported.

Generic BinarySemaphore-based Backend

This module provides a generic backend for pw::sync::TimedThreadNotification via pw_sync:binary_semaphore_timed_thread_notification which uses a pw::sync::BinarySemaphore as the backing primitive. See BinarySemaphore for backend availability.

Optimized Backend

Supported on

Backend module

FreeRTOS

Planned

ThreadX

Planned

embOS

Planned

STL

Not planned, use pw_sync:binary_semaphore_thread_notification

Zephyr

Planned

CMSIS-RTOS API v2 & RTX5

Planned

C++

class pw::sync::TimedThreadNotification
void acquire()

Blocks indefinitely until the thread is notified, i.e. until the notification latch can be cleared because it was set.

Clears the notification latch.

IMPORTANT: This should only be used by a single consumer thread.

bool try_acquire()

Returns whether the thread has been notified, i.e. whether the notificion latch was set and resets the latch regardless.

Clears the notification latch.

Returns true if the thread was notified, meaning the the internal latch was reset successfully.

IMPORTANT: This should only be used by a single consumer thread.

void release()

Notifies the thread in a saturating manner, setting the notification latch.

Raising the notification multiple time without it being acquired by the consuming thread is equivalent to raising the notification once to the thread. The notification is latched in case the thread was not waiting at the time.

This is IRQ and thread safe.

bool try_acquire_for(chrono::SystemClock::duration timeout)

Blocks until the specified timeout duration has elapsed or the thread has been notified (i.e. notification latch can be cleared because it was set), whichever comes first.

Clears the notification latch.

Returns true if the thread was notified, meaning the the internal latch was reset successfully.

IMPORTANT: This should only be used by a single consumer thread.

bool try_acquire_until(chrono::SystemClock::time_point deadline)

Blocks until the specified deadline time has been reached the thread has been notified (i.e. notification latch can be cleared because it was set), whichever comes first.

Clears the notification latch.

Returns true if the thread was notified, meaning the the internal latch was reset successfully.

IMPORTANT: This should only be used by a single consumer thread.

Safe to use in context

Thread

Interrupt

NMI

ThreadNotification::ThreadNotification

ThreadNotification::~ThreadNotification

void ThreadNotification::acquire

bool ThreadNotification::try_acquire

bool ThreadNotification::try_acquire_for

bool ThreadNotification::try_acquire_until

void ThreadNotification::release

Examples in C++
#include "pw_sync/timed_thread_notification.h"
#include "pw_thread/thread_core.h"

class FooHandler() : public pw::thread::ThreadCore {
 // Public API invoked by other threads and/or interrupts.
 void NewFooAvailable() {
   new_foo_notification_.release();
 }

 private:
  pw::sync::TimedThreadNotification new_foo_notification_;

  // Thread function.
  void Run() override {
    while (true) {
      if (new_foo_notification_.try_acquire_for(kNotificationTimeout)) {
        HandleFoo();
      }
      DoOtherStuff();
    }
  }

  void HandleFoo();
  void DoOtherStuff();
}

CountingSemaphore

The CountingSemaphore is a synchronization primitive that can be used for counting events and/or resource management where receiver(s) can block on acquire until notifier(s) signal by invoking release.

Note that unlike Mutexes, priority inheritance is not used by semaphores meaning semaphores are subject to unbounded priority inversions. Due to this, Pigweed does not recommend semaphores for mutual exclusion.

The CountingSemaphore is initialized to being empty or having no tokens.

The entire API is thread safe, but only a subset is interrupt safe. None of it is NMI safe.

Warning

Releasing multiple tokens is often not natively supported, meaning you may end up invoking the native kernel API many times, i.e. once per token you are releasing!

Supported on

Backend module

FreeRTOS

pw_sync_freertos

ThreadX

pw_sync_threadx

embOS

pw_sync_embos

STL

pw_sync_stl

Zephyr

Planned

CMSIS-RTOS API v2 & RTX5

Planned

BinarySemaphore

BinarySemaphore is a specialization of CountingSemaphore with an arbitrary token limit of 1. Note that that max() is >= 1, meaning it may be released up to max() times but only acquired once for those N releases.

Implementations of BinarySemaphore are typically more efficient than the default implementation of CountingSemaphore.

The BinarySemaphore is initialized to being empty or having no tokens.

The entire API is thread safe, but only a subset is interrupt safe. None of it is NMI safe.

Supported on

Backend module

FreeRTOS

pw_sync_freertos

ThreadX

pw_sync_threadx

embOS

pw_sync_embos

STL

pw_sync_stl

Zephyr

Planned

CMSIS-RTOS API v2 & RTX5

Planned

Conditional Variables

We’ve decided for now to skip on conditional variables. These are constructs, which are typically not natively available on RTOSes. CVs would have to be backed by a multiple hidden semaphore(s) in addition to the explicit public mutex. In other words a CV typically ends up as a a composition of synchronization primitives on RTOSes. That being said, one could implement them using our semaphore and mutex layers and we may consider providing this in the future. However for most of our resource constrained customers they will mostly likely be using semaphores more often than CVs.