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 |
|
ThreadX |
|
embOS |
|
STL |
|
Baremetal |
Planned |
Zephyr |
Planned |
CMSIS-RTOS API v2 & RTX5 |
Planned |
C++#
-
class 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. This is thread safe, but NOT IRQ safe.Warning
In order to support global statically constructed Mutexes, 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).
Subclassed by pw::sync::TimedMutex
Public Functions
-
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()#
Attempts 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.
-
void lock()#
Safe to use in context |
Thread |
Interrupt |
NMI |
---|---|---|---|
✔ |
|||
|
✔ |
||
✔ |
|||
✔ |
|||
✔ |
Examples in C++#
Pigweed AI summary: This paragraph provides two examples in C++ for implementing thread-safe critical sections using mutexes. The first example uses the pw_sync/mutex.h library to manually lock and unlock the mutex, while the second example uses C++'s RAII helpers to automatically unlock the mutex. Both examples aim to prevent multiple threads from accessing the same critical section simultaneously.
#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 givenmutex
.
-
bool pw_sync_Mutex_TryLock(pw_sync_Mutex *mutex)#
Invokes the
Mutex::try_lock
member function on the givenmutex
.
-
void pw_sync_Mutex_Unlock(pw_sync_Mutex *mutex)#
Invokes the
Mutex::unlock
member function on the givenmutex
.
Safe to use in context |
Thread |
Interrupt |
NMI |
---|---|---|---|
|
✔ |
||
|
✔ |
||
|
✔ |
Example in C#
Pigweed AI summary: This is an example code in C that demonstrates the use of a mutex to create a thread-safe critical section. The code includes a header file for the mutex and creates an instance of it. The critical section is protected by the mutex using lock and unlock functions.
#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 |
|
ThreadX |
|
embOS |
|
STL |
|
Zephyr |
Planned |
CMSIS-RTOS API v2 & RTX5 |
Planned |
C++#
-
class TimedMutex : public pw::sync::Mutex#
The
TimedMutex
is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads with timeouts and deadlines, extending theMutex
. It offers exclusive, non-recursive ownership semantics where priority inheritance is used to solve the classic priority-inversion problem. This is thread safe, but NOT IRQ safe.Warning
In order to support global statically constructed TimedMutexes, 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).
Public Functions
-
bool try_lock_for(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(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.
-
bool try_lock_for(chrono::SystemClock::duration timeout)#
Safe to use in context |
Thread |
Interrupt |
NMI |
---|---|---|---|
✔ |
|||
|
✔ |
||
✔ |
|||
✔ |
|||
✔ |
|||
✔ |
|||
✔ |
Examples in C++#
Pigweed AI summary: This code snippet provides two examples in C++ for implementing thread-safe critical sections with timeouts. The first example uses the pw_sync::TimedMutex class to lock and unlock the mutex, while the second example uses C++'s RAII helpers to ensure the mutex is always unlocked.
#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.
Functions
-
void pw_sync_TimedMutex_Lock(pw_sync_TimedMutex *mutex)#
Invokes the
TimedMutex::lock
member function on the givenmutex
.
-
bool pw_sync_TimedMutex_TryLock(pw_sync_TimedMutex *mutex)#
Invokes the
TimedMutex::try_lock
member function on the givenmutex
.
-
bool pw_sync_TimedMutex_TryLockFor(pw_sync_TimedMutex *mutex, pw_chrono_SystemClock_Duration timeout)#
Invokes the
TimedMutex::try_lock_for
member function on the givenmutex
.
-
bool pw_sync_TimedMutex_TryLockUntil(pw_sync_TimedMutex *mutex, pw_chrono_SystemClock_TimePoint deadline)#
Invokes the
TimedMutex::try_lock_until
member function on the givenmutex
.
-
void pw_sync_TimedMutex_Unlock(pw_sync_TimedMutex *mutex)#
Invokes the
TimedMutex::unlock
member function on the givenmutex
.
Safe to use in context |
Thread |
Interrupt |
NMI |
---|---|---|---|
✔ |
|||
✔ |
|||
✔ |
|||
✔ |
|||
✔ |
Example in C#
Pigweed AI summary: This is an example code in C that demonstrates how to create a thread-safe critical section with a timeout using the pw_chrono/system_clock.h and pw_sync/timed_mutex.h libraries. The code includes a TimedMutex object and a function that attempts to lock the mutex for a specified duration, executes a critical section, and then unlocks the mutex. The code also includes an external declaration for the TimedMutex object, which can only be created 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;
}
RecursiveMutex#
Pigweed AI summary: The pw_sync library has a RecursiveMutex implementation called pw::sync::RecursiveMutex, but it can only be used internally by Pigweed.
pw_sync
provides pw::sync::RecursiveMutex
, a recursive mutex
implementation. At this time, this facade can only be used internally by
Pigweed.
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 |
|
ThreadX |
|
embOS |
|
STL |
|
Baremetal |
Planned, not ready for use |
Zephyr |
Planned |
CMSIS-RTOS API v2 & RTX5 |
Planned |
C++#
-
class 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.Unlike global interrupt locks, this also works safely and efficiently on SMP systems. On systems which are not SMP, spinning is not required and it’s possible that only interrupt masking occurs but some state may still be used to detect recursion.
This entire API is IRQ safe, but NOT NMI safe.
Precondition: Code that holds a specific
InterruptSpinLock
must not try to re-acquire it. However, it is okay to nest distinct spinlocks.Note
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.Public Functions
-
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 spinlock. Failures are fatal.
Precondition: The spinlock is held by the caller.
-
void lock()#
Safe to use in context |
Thread |
Interrupt |
NMI |
---|---|---|---|
✔ |
✔ |
||
|
✔ |
✔ |
|
✔ |
✔ |
||
✔ |
✔ |
||
✔ |
✔ |
Examples in C++#
Pigweed AI summary: This section provides examples in C++ for interrupt-safe critical sections using both the InterruptSpinLock and C++'s RAII helpers. The code snippets demonstrate how to lock and unlock the interrupt_spin_lock object and how to use the std::lock_guard to ensure the lock is always released.
#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 *spin_lock)#
Invokes the
InterruptSpinLock::lock
member function on the giveninterrupt_spin_lock
.
-
bool pw_sync_InterruptSpinLock_TryLock(pw_sync_InterruptSpinLock *spin_lock)#
Invokes the
InterruptSpinLock::try_lock
member function on the giveninterrupt_spin_lock
.
-
void pw_sync_InterruptSpinLock_Unlock(pw_sync_InterruptSpinLock *spin_lock)#
Invokes the
InterruptSpinLock::unlock
member function on the giveninterrupt_spin_lock
.
Safe to use in context |
Thread |
Interrupt |
NMI |
---|---|---|---|
✔ |
✔ |
||
✔ |
✔ |
||
✔ |
✔ |
Example in C#
Pigweed AI summary: This is an example code in C that includes two header files and defines an interrupt spin lock. It also includes a function called InterruptSafeCriticalSection that uses the interrupt spin lock to ensure thread safety. The code is written in C++ and cannot be created 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#
Pigweed AI summary: This paragraph discusses the steps required to enable Clang's analysis. To enable the analysis, the compilation flag "-Wthread-safety" must be used. Additionally, if STL components like "std::lock_guard" are used, the STL's built-in annotations must be manually enabled by setting the "_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS" macro. If using GN, the "pw_build:clang_thread_safety_warnings" config can be added to the clang toolchain definition's default
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?#
Pigweed AI summary: Lock annotations can be used to prevent race conditions in code that uses locks. It is important to remember to grab and release locks and avoid deadlocks. Clang's lock annotations can inform the compiler and readers of the code about which variables are guarded by which locks, which locks should or should not be held when calling certain functions, and the order in which locks should be acquired.
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_); ... };
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_);
Note
A pointer variable to a shared memory location could itself be a shared variable.
-
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()
andPW_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 withPW_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 that the caller must not hold the given lock. This annotation is often used to prevent deadlocks. Pigweed’s mutex implementation is not re-entrant, so a deadlock will occur if the function acquires the mutex a second time.
Example:
Mutex mu; int a PW_GUARDED_BY(mu); void foo() PW_LOCKS_EXCLUDED(mu) { mu.lock(); ... mu.unlock(); }
-
PW_LOCK_RETURNED(x)#
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 μ } private: Mutex mu; };
-
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 useUNLOCK_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(...)#
-
PW_SHARED_TRYLOCK_FUNCTION(...)#
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 returntrue
on success, orfalse
for functions that returnfalse
on success. The second argument specifies the lock that is locked on success. If unspecified, this lock is assumed to bethis
.
-
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.
-
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#
Pigweed AI summary: This section provides macros for annotating lock objects in order for lock usage annotation to work. The macros can be used to annotate a Lock and a RAII ScopedLocker object. The Lock class includes methods for locking and unlocking, as well as asserting whether the lock is held. The ScopedLocker class includes methods for acquiring and releasing locks, as well as constructors for different lock modes.
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_;
};
Critical Section Lock Helpers#
Virtual Lock Interfaces#
Pigweed AI summary: Virtual lock interfaces are useful when lock selection cannot be templated. Virtual locks enable depending on locks without templating implementation code on the type, while retaining flexibility with respect to the concrete lock type. A case when virtual locks are useful is when the concrete lock type changes at run time. The VirtualBasicLock interface meets the BasicLockable C++ named requirement and offers optional virtual versions of critical section lock primitives.
Virtual lock interfaces can be useful when lock selection cannot be templated.
Why use virtual locks?#
Pigweed AI summary: Virtual locks allow for the use of locks without requiring specific implementation code for each type of lock, while still allowing for flexibility in choosing the type of lock used. This approach aligns with Pigweed's philosophy of avoiding imposing policies on users. Virtual locks are particularly useful when the type of lock needed may change at runtime, such as when accessing flash memory. In such cases, a virtual lock interface can minimize the code-size cost that would result from templating the flash driver.
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#
Pigweed AI summary: The VirtualBasicLock interface is designed to meet the BasicLockable C++ named requirement. It offers optional virtual versions of critical section lock primitives, including VirtualMutex, VirtualTimedMutex, and VirtualInterruptSpinLock.
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#
Pigweed AI summary: The article discusses the trade-offs between internal and external locking in programming. Internal locking ensures that concurrent calls to public member functions do not corrupt an instance of a class, but can result in increased code size and difficulty in performing multi-method thread-safe transactions. External locking, on the other hand, exposes the lock to the caller and can be used externally to the public API, but can be error-prone when instantiating and passing around instances and their locks. The article introduces Borrowable as a solution to
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?#
Pigweed AI summary: Borrowable is a code-size efficient way to enable external locking that is easy and safe to use. It holds references to a protected instance and its lock, providing RAII-style access. This construct is useful for sharing objects or data that are transactional in nature, and for separating timeout constraints between the acquiring of the shared object and timeouts used for the shared object's API. Borrowable has semantics similar to a pointer and should be passed by value. It can also be assigned to a subclass.
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
.
Note
Borrowable
has semantics similar to a pointer and should be passed by
value. Furthermore, a Borrowable<U>
can be assigned to a
Borrowable<T>
if U
is a subclass of T
.
C++#
-
template<typename GuardedType, typename Lock = pw::sync::VirtualBasicLockable>
class 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.Public Functions
-
inline ~BorrowedPointer()#
Release the lock on destruction.
-
inline BorrowedPointer(BorrowedPointer &&other)#
This object is moveable, but not copyable.
Postcondition: The other BorrowedPointer is no longer valid and will assert if the GuardedType is accessed.
-
inline GuardedType *operator->()#
Provides access to the borrowed object’s members.
-
inline const GuardedType *operator->() const#
Const overload.
-
inline GuardedType &operator*()#
Provides access to the borrowed object directly.
Note
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.
-
inline const GuardedType &operator*() const#
Const overload.
-
inline ~BorrowedPointer()#
-
template<typename GuardedType, typename Lock = pw::sync::VirtualBasicLockable>
class Borrowable# The
Borrowable
is a helper construct that enables callers to borrow an object which is guarded by a lock.Users who need access to the guarded object can ask to acquire a
BorrowedPointer
which permits access while the lock is held.Thread-safety analysis is not supported for this class, as the
BorrowedPointer
s it creates conditionally releases the lock. See also https://clang.llvm.org/docs/ThreadSafetyAnalysis.html#no-conditionally-held-locksThis class is compatible with locks which comply with
BasicLockable
,Lockable
, andTimedLockable
C++ named requirements.Borrowable<T>
is covariant with respect toT
, so thatBorrowable<U>
can be converted toBorrowable<T>
, ifU
is a subclass ofT
.Borrowable
has pointer-like semantics and should be passed by value.Subclassed by pw::sync::InlineBorrowable< GuardedType, Lock, LockInterface >
Public Functions
-
inline BorrowedPointer<GuardedType, Lock> acquire() const#
Blocks indefinitely until the object can be borrowed. Failures are fatal.
-
template<int&... ExplicitArgumentBarrier, typename T = Lock, typename = std::enable_if_t<is_lockable_v<T>>>
inline std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire() const# Tries to borrow the object in a non-blocking manner. Returns a BorrowedPointer on success, otherwise
std::nullopt
(nothing).
-
template<class Rep, class Period, int&... ExplicitArgumentBarrier, typename T = Lock, typename = std::enable_if_t<is_lockable_for_v<T, std::chrono::duration<Rep, Period>>>>
inline std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire_for(std::chrono::duration<Rep, Period> timeout) const# 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, otherwisestd::nullopt
(nothing).
-
template<class Clock, class Duration, int&... ExplicitArgumentBarrier, typename T = Lock, typename = std::enable_if_t<is_lockable_until_v<T, std::chrono::time_point<Clock, Duration>>>>
inline std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire_until(std::chrono::time_point<Clock, Duration> deadline) const# Tries to borrow the object. Blocks until the specified deadline has passed or the object has been borrowed, whichever comes first. Returns a
BorrowedPointer
on success, otherwisestd::nullopt
(nothing).
-
inline BorrowedPointer<GuardedType, Lock> acquire() const#
Example in C++#
Pigweed AI summary: This is an example code in C++ that demonstrates how to use the pw_bytes/span.h, pw_i2c/initiator.h, pw_status/try.h, pw_status/result.h, pw_sync/borrow.h, and pw_sync/mutex.h libraries to read data from an I2C bus. The code creates an ExampleI2c class that inherits from pw::i2c::Initiator and uses a VirtualMutex and Borrowable to synchronize access to the I2C bus. The
#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;
}
InlineBorrowable#
InlineBorrowable
is a helper to simplify the common use case where an object
is wrapped in a Borrowable
for its entire lifetime. The InlineBorrowable
owns the guarded object and the lock object.
InlineBorrowable has a separate parameter for the concrete lock type
that is instantiated and a (possibly virtual) lock interface type that is
referenced by users of the guarded object. The default lock is
pw::sync::VirtualMutex
and the default lock interface is
pw::sync::VirtualBasicLockable
.
An InlineBorrowable is a Borrowable with the same guarded object and lock interface types, and it can be passed directly to APIs that expect a Borrowable reference.
Why use InlineBorrowable?#
Pigweed AI summary: InlineBorrowable is a safer and simpler way to protect an object for its entire lifetime. The object is never exposed and doesn't need to be stored separately. The lock and the guarded object have the same lifetime, and the lock cannot be used for any other purpose.
It is a safer and simpler way to guard an object for its entire lifetime. The unguarded object is never exposed and doesn’t need to be stored in a separate variable or data member. The guarded object and its lock are guaranteed to have the same lifetime, and the lock cannot be re-used for any other purpose.
Constructing objects in-place#
Pigweed AI summary: This section explains how to construct objects in-place using the InlineBorrowable class. The guarded object and its lock are constructed in-place, and any constructor parameters must be passed through the InlineBorrowable constructor. There are three ways to do this: passing parameters inline to the constructor, passing parameters inside tuples, or using callables to construct the objects. The section provides code examples for each approach and notes that the last approach allows for constructing and returning non-copyable or non-movable objects.
The guarded object and its lock are constructed in-place by the InlineBorrowable, and any constructor parameters required by the object or its lock must be passed through the InlineBorrowable constructor. There are several ways to do this:
Pass the parameters for the guarded object inline to the constructor. This is the recommended way to construct the object when the lock does not require any constructor parameters. Use the
std::in_place
marker to invoke the inline constructor.InlineBorrowable<Foo> foo(std::in_place, foo_arg1, foo_arg2); InlineBorrowable<std::array<int, 2>> foo_array(std::in_place, 1, 2);
Pass the parameters inside tuples:
InlineBorrowable<Foo> foo(std::forward_as_tuple(foo_arg1, foo_arg2)); InlineBorrowable<Foo, MyLock> foo_lock( std::forward_as_tuple(foo_arg1, foo_arg2), std::forward_as_tuple(lock_arg1, lock_arg2));
Note
This approach only supports list initialization starting with C++20.
Use callables to construct the guarded object and lock object:
InlineBorrowable<Foo> foo([&]{ return Foo{foo_arg1, foo_arg2}; }); InlineBorrowable<Foo, MyLock> foo_lock( [&]{ return Foo{foo_arg1, foo_arg2}; } [&]{ return MyLock{lock_arg1, lock_arg2}; }
Note
It is possible to construct and return objects that are not copyable or movable, thanks to mandatory copy ellision (return value optimization).
C++#
-
template<typename GuardedType, typename Lock = pw::sync::VirtualMutex, typename LockInterface = pw::sync::VirtualBasicLockable>
class InlineBorrowable : private internal::BorrowableStorage<GuardedType, pw::sync::VirtualMutex>, public pw::sync::Borrowable<GuardedType, pw::sync::VirtualBasicLockable># InlineBorrowable
holds an object ofGuardedType
and a Lock that guards access to the object. It should be used when an object should be guarded for its entire lifecycle by a single lock.This object should be shared with other componetns as a reference of type
Borrowable<GuardedType, LockInterface>
.Public Functions
-
inline constexpr InlineBorrowable()#
Construct the guarded object and lock using their default constructors.
-
template<typename ...Args>
inline explicit constexpr InlineBorrowable(std::in_place_t, Args&&... args)# Construct the guarded object by providing its constructor arguments inline. The lock is constructed using its default constructor.
This constructor supports list initialization for arrays, structs, and other objects such as
std::array
.Example:
InlineBorrowable<Foo> foo(std::in_place, foo_arg1, foo_arg2); InlineBorrowable<std::array<int, 2>> foo_array(std::in_place, 1, 2);
-
template<typename ...ObjectArgs, typename ...LockArgs>
inline explicit constexpr InlineBorrowable(std::tuple<ObjectArgs...> &&object_args, std::tuple<LockArgs...> &&lock_args = std::make_tuple())# Construct the guarded object and lock by providing their construction parameters using separate tuples. The 2nd tuple can be ommitted to construct the lock using its default constructor.
Example:
InlineBorrowable<Foo> foo(std::forward_as_tuple(foo_arg1, foo_arg2)); InlineBorrowable<Foo, MyLock> foo_lock( std::forward_as_tuple(foo_arg1, foo_arg2), std::forward_as_tuple(lock_arg1, lock_arg2));
Note
This constructor only supports list initialization with C++20 or later, because it requires https://wg21.link/p0960.
-
template<typename ObjectConstructor, typename LockConstructor = Lock(), typename = std::enable_if_t<std::is_invocable_r_v<GuardedType&&, ObjectConstructor>>, typename = std::enable_if_t<std::is_invocable_r_v<Lock&&, LockConstructor>>>
inline explicit constexpr InlineBorrowable(const ObjectConstructor &object_ctor, const LockConstructor &lock_ctor = internal::DefaultConstruct<Lock>)# Construct the guarded object and lock by providing factory functions. The 2nd callable can be ommitted to construct the lock using its default constructor.
Example:
InlineBorrowable<Foo> foo([&]{ return Foo{foo_arg1, foo_arg2}; }); InlineBorrowable<Foo, MyLock> foo_lock( [&]{ return Foo{foo_arg1, foo_arg2}; } [&]{ return MyLock{lock_arg1, lock_arg2}; }
-
inline constexpr InlineBorrowable()#
Example in C++#
Pigweed AI summary: This is an example code in C++ that includes various libraries and defines a class called ExampleI2c that inherits from pw::i2c::Initiator. It also defines a function called ReadI2cData that takes a pw::sync::Borrowable<pw::i2c::Initiator> as an argument and returns a pw::Result<ConstByteSpan>. Another function called ReadData is defined that calls ReadI2cData with an instance of ExampleI2c
#include <utility>
#include "pw_bytes/span.h"
#include "pw_i2c/initiator.h"
#include "pw_status/result.h"
#include "pw_sync/inline_borrowable.h"
struct I2cOptions;
class ExampleI2c : public pw::i2c::Initiator {
public:
ExampleI2c(int bus_id, I2cOptions options);
// ...
};
int kBusId;
I2cOptions opts;
pw::sync::InlineBorrowable<ExampleI2c> i2c(std::in_place, kBusId, opts);
pw::Result<ConstByteSpan> ReadI2cData(
pw::sync::Borrowable<pw::i2c::Initiator> initiator,
ByteSpan buffer);
pw::Result<ConstByteSpan> ReadData(ByteSpan buffer) {
return ReadI2cData(i2c, 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 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.
The ThreadNotification
is initialized to being empty (latch is not
set).
Generic BinarySemaphore-based Backend#
Pigweed AI summary: This module provides a generic backend for pw::sync::ThreadNotification using pw_sync:binary_semaphore_thread_notification, which uses pw::sync::BinarySemaphore as the backing primitive. The availability of the backend can be found in BinarySemaphore.
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#
Pigweed AI summary: This paragraph describes the supported optimized backend module for various operating systems, including FreeRTOS, ThreadX, embOS, STL, Baremetal, Zephyr, and CMSIS-RTOS API v2 & RTX5. It provides information on which modules are possible to use and which ones are planned for future use.
Supported on |
Optimized backend module |
---|---|
FreeRTOS |
|
ThreadX |
Not possible, use |
embOS |
Not needed, use |
STL |
Not planned, use |
Baremetal |
Planned |
Zephyr |
Planned |
CMSIS-RTOS API v2 & RTX5 |
Planned |
C++#
-
class 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.IMPORTANT: 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 a binary semaphore 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.
The
ThreadNotification
is initialized to being empty (latch is not set).Subclassed by pw::sync::TimedThreadNotification
Public Functions
-
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.
-
void acquire()#
Safe to use in context |
Thread |
Interrupt |
NMI |
---|---|---|---|
✔ |
|||
|
✔ |
||
✔ |
|||
✔ |
|||
✔ |
✔ |
Examples in C++#
Pigweed AI summary: This is an example code in C++ that includes two header files and defines a class called FooHandler. The class inherits from ThreadCore and has a public API function called NewFooAvailable() that releases a thread notification. The class also has a private member variable called new_foo_notification_ which is a ThreadNotification object. The class has a Run() function that runs in a loop and waits for the new_foo_notification_ to be acquired before calling the HandleFoo() function.
#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.
The TimedThreadNotification
is initialized to being empty (latch is
not set).
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#
Pigweed AI summary: This module provides a generic backend for TimedThreadNotification using BinarySemaphore as the backing primitive. The backend is available through pw_sync:binary_semaphore_timed_thread_notification and more information can be found in the BinarySemaphore module.
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#
Pigweed AI summary: This section provides information on the optimized backend module and its support for various operating systems. It includes a table that lists the supported operating systems and the corresponding backend modules. FreeRTOS is supported by "pw_sync_freertos:timed_thread_notification", while ThreadX is not supported and requires the use of "pw_sync:binary_semaphore_timed_thread_notification". EmbOS is not needed as it can use "pw_sync:binary_semaphore_timed_thread_notification". STL is not planned for support and also requires
Supported on |
Backend module |
---|---|
FreeRTOS |
|
ThreadX |
Not possible, use |
embOS |
Not needed, use |
STL |
Not planned, use |
Zephyr |
Planned |
CMSIS-RTOS API v2 & RTX5 |
Planned |
C++#
-
class TimedThreadNotification : public pw::sync::ThreadNotification#
The
TimedThreadNotification
is a synchronization primitive that can be used to permit a SINGLE thread to block and consume a latching, saturating notification from multiple notifiers.IMPORTANT: 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 a binary semaphore 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.
The
TimedThreadNotification
is initialized to being empty (latch is not set).Public Functions
-
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.
-
bool try_acquire_for(chrono::SystemClock::duration timeout)#
Safe to use in context |
Thread |
Interrupt |
NMI |
---|---|---|---|
✔ |
|||
|
✔ |
||
|
✔ |
||
|
✔ |
||
✔ |
|||
✔ |
|||
|
✔ |
✔ |
Examples in C++#
Pigweed AI summary: This is an example code in C++ that includes two header files and defines a class called FooHandler. The class inherits from pw::thread::ThreadCore and has a public API called NewFooAvailable() that is invoked by other threads and/or interrupts. The class also has a private member called new_foo_notification_ which is an instance of pw::sync::TimedThreadNotification. The class has a Run() function that runs in a loop and checks if new_foo_notification_ has been
#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 Mutex
, 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.
Note
If there is only a single consuming thread, we recommend using a
ThreadNotification
instead which can be much more efficient on
some RTOSes such as FreeRTOS.
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 |
|
ThreadX |
|
embOS |
|
STL |
|
Zephyr |
Planned |
CMSIS-RTOS API v2 & RTX5 |
Planned |
C++#
-
class 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. Pigweed does not recommend semaphores for mutual exclusion. The entire API is thread safe but only a subset is IRQ safe.The
CountingSemaphore
is initialized to being empty or having no tokens.Warning
In order to support global statically constructed
CountingSemaphores
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).Public Functions
-
void release(ptrdiff_t update = 1)#
Atomically increments the internal counter by the value of update. Any thread(s) waiting for the counter to be greater than 0, i.e. blocked in acquire, will subsequently be unblocked. This is IRQ safe.
Precondition: update >= 0
Precondition: update <= max() - counter
-
void acquire()#
Decrements the internal counter by 1 or blocks indefinitely until it can.
This is thread safe, but not IRQ safe.
-
bool try_acquire() noexcept#
Tries to decrement by the internal counter by 1 without blocking. Returns true if the internal counter was decremented successfully.
This is IRQ safe.
-
bool try_acquire_for(chrono::SystemClock::duration timeout)#
Tries to decrement the internal counter by 1. Blocks until the specified timeout has elapsed or the counter was decremented by 1, whichever comes first.
Returns true if the internal counter was decremented successfully. This is thread safe, but not IRQ safe.
-
bool try_acquire_until(chrono::SystemClock::time_point deadline)#
Tries to decrement the internal counter by 1. Blocks until the specified deadline has been reached or the counter was decremented by 1, whichever comes first.
Returns true if the internal counter was decremented successfully.
This is thread safe, but not IRQ safe.
Public Static Functions
-
static inline constexpr ptrdiff_t max() noexcept#
Returns the internal counter’s maximum possible value.
-
void release(ptrdiff_t update = 1)#
Safe to use in context |
Thread |
Interrupt |
NMI |
---|---|---|---|
✔ |
|||
|
✔ |
||
✔ |
|||
✔ |
✔ |
||
✔ |
|||
✔ |
|||
✔ |
✔ |
||
✔ |
✔ |
✔ |
Examples in C++#
Pigweed AI summary: This paragraph provides an example of using a counting semaphore in C++ to run periodic tasks at frequencies near or higher than the system clock tick rate. The code includes a class called PeriodicWorker that uses a counting semaphore to wait until it's time to run, and then performs periodic work. The code also includes a function to log a warning message if the worker falls behind schedule.
As an example, a counting sempahore can be useful to run periodic tasks at frequencies near or higher than the system clock tick rate in a way which lets you detect whether you ever fall behind.
#include "pw_sync/counting_semaphore.h"
#include "pw_thread/thread_core.h"
class PeriodicWorker() : public pw::thread::ThreadCore {
// Public API invoked by a higher frequency timer interrupt.
void TimeToExecute() {
periodic_run_semaphore_.release();
}
private:
pw::sync::CountingSemaphore periodic_run_semaphore_;
// Thread function.
void Run() override {
while (true) {
size_t behind_by_n_cycles = 0;
periodic_run_semaphore_.acquire(); // Wait to run until it's time.
while (periodic_run_semaphore_.try_acquire()) {
++behind_by_n_cycles;
}
if (behind_by_n_cycles > 0) {
PW_LOG_WARNING("Not keeping up, behind by %d cycles",
behind_by_n_cycles);
}
DoPeriodicWork();
}
}
void DoPeriodicWork();
}
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.
Note
If there is only a single consuming thread, we recommend using a ThreadNotification instead which can be much more efficient on some RTOSes such as FreeRTOS.
Supported on |
Backend module |
---|---|
FreeRTOS |
|
ThreadX |
|
embOS |
|
STL |
|
Zephyr |
Planned |
CMSIS-RTOS API v2 & RTX5 |
Planned |
C++#
-
class BinarySemaphore#
BinarySemaphore
is a specialization ofCountingSemaphore
with an arbitrary token limit of 1. Note that that max() is >= 1, meaning it may be released up tomax()
times but only acquired once for thoseN
releases. Implementations ofBinarySemaphore
are typically more efficient than the default implementation ofCountingSemaphore
. The entire API is thread safe but only a subset is IRQ safe.WARNING: In order to support global statically constructed BinarySemaphores, 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).
The
BinarySemaphore
is initialized to being empty or having no tokens.Public Functions
-
void release()#
Atomically increments the internal counter by 1. Any thread(s) waiting for the counter to be greater than 0, i.e. blocked in acquire, will subsequently be unblocked. This is thread and IRQ safe.
There exists an overflow risk if one releases more than max() times between acquires because many RTOS implementations internally increment the counter past one where it is only cleared when acquired.
PRECONDITION:
1 <= max() - counter
-
void acquire()#
Decrements the internal counter to 0 or blocks indefinitely until it can.
This is thread safe, but not IRQ safe.
-
bool try_acquire() noexcept#
Tries to decrement by the internal counter to 0 without blocking.
This is thread and IRQ safe.
- Return values:
true – if the internal counter was reset successfully.
-
bool try_acquire_for(chrono::SystemClock::duration timeout)#
Tries to decrement the internal counter to 0. Blocks until the specified timeout has elapsed or the counter was decremented to 0, whichever comes first.
This is thread safe, but not IRQ safe.
- Return values:
true – if the internal counter was decremented successfully.
-
bool try_acquire_until(chrono::SystemClock::time_point deadline)#
Tries to decrement the internal counter to 0. Blocks until the specified deadline has been reached or the counter was decremented to 0, whichever comes first.
This is thread safe, but not IRQ safe.
- Return values:
true – if the internal counter was decremented successfully.
Public Static Functions
-
static inline constexpr ptrdiff_t max() noexcept#
- Return values:
backend::kBinarySemaphoreMaxValue – the internal counter’s maximum possible value.
-
void release()#
Safe to use in context |
Thread |
Interrupt |
NMI |
---|---|---|---|
✔ |
|||
|
✔ |
||
✔ |
|||
✔ |
✔ |
||
✔ |
|||
✔ |
|||
✔ |
✔ |
||
✔ |
✔ |
✔ |
Examples in C++#
Pigweed AI summary: This is a code example in C++ that demonstrates the use of binary semaphores and thread cores. The code creates a class called FooHandler that extends the ThreadCore class and includes a public API for other threads and interrupts to invoke. The class also includes a binary semaphore and a thread function that waits for the semaphore to be released before executing the HandleFoo() function. The code also includes a DoOtherStuff() function that is executed when the semaphore is not released.
#include "pw_sync/binary_semaphore.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_semaphore_.release();
}
private:
pw::sync::BinarySemaphore new_foo_semaphore_;
// Thread function.
void Run() override {
while (true) {
if (new_foo_semaphore_.try_acquire_for(kNotificationTimeout)) {
HandleFoo();
}
DoOtherStuff();
}
}
void HandleFoo();
void DoOtherStuff();
}
Conditional Variables#
Pigweed AI summary: The pw::sync::ConditionVariable offers an implementation of a condition variable that has similar semantics and API to the std::condition_variable in the C++ Standard Library.
pw::sync::ConditionVariable
provides a condition variable
implementation that provides semantics and an API very similar to
std::condition_variable in the C++
Standard Library.