Mark Wilson

April 3, 2026

3m 37s

Why Sharing Objects Across Threads Breaks Embedded Systems

Why Sharing Objects Across Threads Breaks Embedded Systems

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.

The Pattern

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:

  • std::shared_ptr ensures the object remains alive
  • components can “just use” the object 
  • no explicit data movement is required 

The code appears modern and well-factored, but this approach is naive.

What Actually Happens

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.

1. Hidden Concurrency

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.

2. Uncontrolled Mutation

Shared objects are often mutable. Multiple threads may update the same object, introducing race conditions or requiring complex synchronization.

3. Coupling Through State

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.

4. Fragile Shutdown

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 Underlying Issue: Entities vs. Values

The real cause of these issues is confusion between two different kinds of things.

Entities

Entities have identity and a lifetime.

Examples include:

  • devices 
  • controllers 
  • long-lived system components 

Entities are not interchangeable. They represent something that exists over time and must be owned and managed explicitly.

Values

Values are data.

Examples include:

  • measurements 
  • messages 
  • events 

Values can be copied, moved, and passed freely. They do not have identity. They represent information, not ownership.

Where Systems Go Wrong

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:

  • lifetime is shared 
  • mutation is shared 
  • execution context is unclear 

In other words, everything is shared except responsibility.

A Value-Oriented Approach

The solution is to separate values and entities clearly.

Values Flow

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

Entities Are Owned

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:

  • its lifetime 
  • its mutation 
  • its interaction with the outside world 

Entities are not shared across threads. If another part of the system needs information, it receives a value.

Concurrency Boundaries Are Explicit

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.

What this Does

This approach gives us some practical improvements.

Systems Become Analyzable

It becomes much easier to look at the code and understand thread ownership, how information flows, and where concurrency boundaries exist.

Race Conditions Are Reduced

By eliminating shared mutable state, many race conditions disappear by design. Synchronization becomes simpler or even unnecessary.

Shutdown Becomes Predictable

Each thread owns its entities. Shutdown is a matter of signaling threads and allowing them to clean up their own state.

Debugging Improves

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.

Conclusion

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:

  • values flow through the system as data 
  • entities are owned and managed explicitly 

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.