How synchronizers work
Every synchronizer in Basis inherits fromSynchronizerBase<T_MSG_CONTAINERs...>:
MessageSumType tuple. When OnMessage<INDEX>() is called, the message is stored. IsReadyNoLock() is then checked. If ready, ConsumeMessagesNoLock() extracts all buffered messages and returns them as a MessageSumType tuple.
Consumed messages are cleared from storage — unless the input is marked cached (see below).
Thread safety
All synchronizer operations are protected by an internalstd::mutex. Messages can arrive concurrently from multiple subscriber callbacks; the synchronizer ensures correctness.
Input storage modifiers
Two per-input modifiers change how the synchronizer buffers messages.optional
An optional input does not block the synchronizer from firing. The synchronizer checks readiness as if that input were always filled. If a message has arrived by the time the handler fires, it is included; otherwise the field is empty (nullptr for shared pointers).
cached
A cached input is not cleared after the handler fires. The most recently received message is reused in every subsequent handler invocation until a new message arrives.
cached: true is set per-input.
accumulate
When an input is declared with accumulate, its container type is a std::vector<std::shared_ptr<const T_MSG>> instead of a single std::shared_ptr<const T_MSG>. Messages accumulate in the vector between handler invocations.
MessageSumType tuple, so your handler receives all accumulated messages at once.
Synchronizer types
all — all inputs received
The All synchronizer fires when every non-optional input has at least one message buffered.
all when you want to process the latest message from each topic, regardless of timestamps. For a single-input handler this is equivalent to “fire on every message”.
equal — exact field matching
The equal synchronizer (FieldSyncEqual) fires when a designated field value is identical across all synced inputs. Unsynced inputs (those with no sync_field) are matched after the synced inputs have found a match.
FieldSync buffers messages for each synced input. When a message arrives on a synced input, it searches each other synced input’s buffer for a message whose field value satisfies the operator. If all required synced inputs have a match, the matching messages are consumed together.
sync_field can be:
- A member access expression on the raw message pointer:
header.stamp()calls a method. - A raw field with the
::prefix:::timeaccesses a direct struct field.
approximate — approximate equality within a threshold
The approximate synchronizer (FieldSyncApproximatelyEqual) fires when the synced fields of all inputs are within a configurable epsilon of each other.
basis_example.cpp):
buffer_size field caps how many messages per input channel are held before older messages are dropped.
rate — time-based firing
The rate synchronizer fires at a fixed frequency regardless of message arrival. It is driven by a RateSubscriber timer thread rather than by incoming messages.
When the timer fires, the synchronizer calls ConsumeIfReady(). If all non-optional inputs have at least one cached message, the handler fires with those messages. The inputs act as a cache — their last received values are forwarded to the handler each tick.
/topic_a has been received at least once. /topic_b is passed along if available.
Rate is declared in the handler’s
sync block, not in the input. The rate determines when the synchronizer is polled — the inputs still accumulate between polls.10hz, 100ms, 0.1s.
external — externally triggered
An external synchronizer has no automatic trigger. The handler is called only when something outside the Basis pub-sub system explicitly requests it. This is useful for Units that bridge external sensors or callbacks (for example, a camera frame callback from an SDK).
RunHandlerAndPublish() directly when data is available.
buffer_size
The buffer_size field on a sync block caps the per-channel message buffer used by equal and approximate synchronizers. When the buffer is full, incoming messages overwrite the oldest entry.
all synchronizer, buffer_size is not applicable — all holds only the most recent message per channel.
Using synchronizers directly in C++
You can instantiate and use synchronizers directly without code generation. Thebasis_example.cpp example shows the approximate synchronizer used manually:
OnMessage<INDEX>() returns true if the synchronizer became ready. You can use that return value instead of (or in addition to) calling ConsumeIfReady().
Synchronizer type summary
| YAML type | C++ class | Trigger condition |
|---|---|---|
all | basis::synchronizers::All<...> | All non-optional inputs have a message |
equal | basis::synchronizers::FieldSyncEqual<...> | Designated fields are exactly equal |
approximate: N | basis::synchronizers::FieldSyncApproximatelyEqual<EPSILON, ...> | Designated fields are within epsilon |
rate: Nhz | All<...> polled by RateSubscriber | Fixed-period timer fires and all required inputs are cached |
external | (none — handler called directly) | Triggered by external code |
Next steps
Pub-sub messaging
How publishers and subscribers deliver messages to the synchronizer
Units
How HandlerPubSub integrates the synchronizer with publishers and subscribers
Unit YAML schema
Full reference for all sync fields, input options, and output options
Transport layers
How messages reach the synchronizer from inproc and network transports