Skip to main content

Topic Design

For the basic usage, see the Topic, SyncSubscriber, ASyncSubscriber, and QueuedSubscriber pages in the message system section. This page explains why the mechanism is split into these roles.

What Topic is solving

Topic is not trying to be a message bus that carries everything. Its goal is to unify the most common in-process handoff patterns with as little overhead as possible: publishers write data, subscribers consume it in different ways, the latest value may be cached when needed, and multi-publisher protection can be enabled when needed. It does not pull in Linux shared memory, process-to-process synchronization, or complex durable queue semantics because those are outside the boundary of this lightweight path.

The role of Block

At the source level, Block is the central structure inside Topic. It stores the current data view, the maximum length, the subscriber lists, and the state used to coordinate concurrent access. By default it optimizes the single-publisher path: if multi_publisher is not explicitly enabled, it only uses a lightweight atomic busy state for serialization. Only when multi-publisher mode is enabled does it fall back to Mutex. That boundary matters, because it shows Topic is designed for the common single-publisher case first instead of dragging every publish through a heavier synchronization primitive.

Why cache is optional

Cache follows the same idea. Topic does not force itself to hold a separate stable copy. Cache can be enabled when needed. Without cache, the publish path only exposes the current data view through Block. With cache enabled, Topic keeps a stable internal copy. If the upper layer needs "the latest stable value", cache is turned on explicitly. If it only needs a one-time handoff, there is no reason to pay the copy cost by default.

Why subscribers are split by type

The subscriber types are split into synchronous, asynchronous, queued, and callback variants because they represent four genuinely different consumption semantics. SyncSubscriber means "wake me when new data arrives". ASyncSubscriber is closer to "I will fetch the latest result later". QueuedSubscriber means "enqueue every publish". Callback subscription means "invoke me immediately at publish time". If all of that were collapsed into one subscriber interface, the result would either degenerate into the most conservative common subset or push too many branches into runtime.

Why it is not a strict message queue

From a concurrency point of view, Topic is better read as a framework that dispatches published data into different consumption forms rather than as a strict message queue. The synchronous path uses Semaphore, the asynchronous path uses small state blocks, the queue path uses LockFreeQueue, and callback subscribers are linked through LockFreeList. That makes it a good fit for in-process module handoff, log fan-out, or state broadcast. If the requirement is shared large payloads across processes, explicit queue-full policy, or zero-copy shared slots, then the right move is to switch to LinuxSharedTopic<T> instead of pushing system-level semantics back into Topic.

WaitTopic and domain

Another point that is easy to miss is WaitTopic and domain. Topic is not named on a single global flat plane. It can be organized by domain, and WaitTopic is not about "get the object immediately". It means waiting for a topic to appear in the matching domain. This solves module organization when initialization order is not completely fixed. It is lightweight in-process discovery and binding, not a service registry and not a cross-process directory.

Positioning

In one sentence, Topic is an in-process publish-subscribe path with relatively low semantic burden and multiple consumption forms. It is best at letting modules exchange data without exposing too much of each other's lifecycle. It is not trying to solve every message-system problem at once.