April 3, 2026
3m 37s

In many embedded systems, shared objects can hide critical behavior issues.
A common pattern is to represent devices, state, or control logic as objects that are shared across threads. These objects are often managed with std::shared_ptr, giving the impression that ownership and lifetime are handled correctly.
The system compiles and runs. It may even pass basic tests.
But under load, or during shutdown, problems begin to surface. Behavior becomes difficult to reason about. Bugs are intermittent. Fixes introduce new issues elsewhere.
The root of the problem is not syntax or tooling. It is deeper than that: sharing objects across threads obscures ownership, execution context, and mutation.
Let’s say a system has multiple threads: sensor input, processing, logging, and control. Instead of passing data between those threads explicitly, objects are shared.
A pointer to a device object might be wrapped in a std::shared_ptr and passed to multiple components. A state object might be updated in place by different threads. Callbacks may be invoked from threads that are not visible at the call site.
This seems reasonable because:
The code appears modern and well-factored, but this approach is naive.
std::shared_ptr answers the question:
“Will this object stay alive (i.e., not destroyed)?”
It does not address ownership of the object, which other objects are allowed to modify it, or when it is safe to do so. Nor does it specify anything about how shutdown of the system works.
As a result, several failure modes emerge.
An object may be accessed from multiple threads without any clear boundary. The execution context is implicit; at any given call site, it is not obvious which thread is running.
Shared objects are often mutable. Multiple threads may update the same object, introducing race conditions or requiring complex synchronization.
Components become coupled through shared objects rather than explicit interfaces. Changes in one part of the system affect others in ways that are not visible in the type structure.
Shutdown may become particularly troublesome. Threads may still hold references to shared objects. Destruction order becomes unpredictable. Systems hang or crash during teardown.
All these issues are very difficult to debug and clean up.
The real cause of these issues is confusion between two different kinds of things.
Entities have identity and a lifetime.
Examples include:
Entities are not interchangeable. They represent something that exists over time and must be owned and managed explicitly.
Values are data.
Examples include:
Values can be copied, moved, and passed freely. They do not have identity. They represent information, not ownership.
Problems arise when entities are treated like values.
An entity is wrapped in a std::shared_ptr and passed around as if it were just another piece of data. Multiple threads access it. Its lifetime is shared, but its ownership is not defined.
This creates a system where:
In other words, everything is shared except responsibility.
The solution is to separate values and entities clearly.
Measurements and messages are treated as value types. They are copied or moved between threads, typically through queues.
Each thread operates on its own data. There is no shared mutable state
Entity ownership is established early and explicitly in the system’s lifetime.
A thread (or a clearly defined component) owns an entity and is responsible for:
Entities are not shared across threads. If another part of the system needs information, it receives a value.
Queues define concurrency boundaries. Data crosses threads as values moved through a queue. Execution context is visible; there is no ambiguity about where code runs or who owns what.
This approach gives us some practical improvements.
It becomes much easier to look at the code and understand thread ownership, how information flows, and where concurrency boundaries exist.
By eliminating shared mutable state, many race conditions disappear by design. Synchronization becomes simpler or even unnecessary.
Each thread owns its entities. Shutdown is a matter of signaling threads and allowing them to clean up their own state.
When something goes wrong, you can reason about it locally. Behavior is not spread across threads through shared objects. Unit tests are easier to write and run.
Sharing objects across threads often feels convenient, especially when using tools like std::shared_ptr. It can make lifetime appear safe while leaving ownership, mutation, and execution context undefined.
The result is systems that are difficult to reason about, especially under real-world conditions.
A clearer model is to distinguish between values and entities:
This separation makes behavior visible. It reduces coupling. It leads to systems that are easier to understand, debug, and maintain.
Shared ownership is not the same as clear ownership. In concurrent embedded systems, that distinction matters.